aboutsummaryrefslogtreecommitdiff
path: root/src/dutil/dlutil.cpp
diff options
context:
space:
mode:
authorRob Mensching <rob@firegiant.com>2017-09-03 11:22:38 -0700
committerRob Mensching <rob@firegiant.com>2017-09-03 13:33:33 -0700
commit5d8375007754101ff2889d0e79486c8f9b7cf5ab (patch)
treea76d6fb6a38dd9f04a93ffcfd9d64e76779b3414 /src/dutil/dlutil.cpp
parent8e8da6dbc051ec884b5d439bb4f44dc027d05bbf (diff)
downloadwix-5d8375007754101ff2889d0e79486c8f9b7cf5ab.tar.gz
wix-5d8375007754101ff2889d0e79486c8f9b7cf5ab.tar.bz2
wix-5d8375007754101ff2889d0e79486c8f9b7cf5ab.zip
Initial commit
Diffstat (limited to 'src/dutil/dlutil.cpp')
-rw-r--r--src/dutil/dlutil.cpp783
1 files changed, 783 insertions, 0 deletions
diff --git a/src/dutil/dlutil.cpp b/src/dutil/dlutil.cpp
new file mode 100644
index 00000000..81455df0
--- /dev/null
+++ b/src/dutil/dlutil.cpp
@@ -0,0 +1,783 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3#include "precomp.h"
4
5#include <inetutil.h>
6#include <uriutil.h>
7
8static const DWORD64 DOWNLOAD_ENGINE_TWO_GIGABYTES = DWORD64(2) * 1024 * 1024 * 1024;
9static LPCWSTR DOWNLOAD_ENGINE_ACCEPT_TYPES[] = { L"*/*", NULL };
10
11// internal function declarations
12
13static HRESULT InitializeResume(
14 __in LPCWSTR wzDestinationPath,
15 __out LPWSTR* psczResumePath,
16 __out HANDLE* phResumeFile,
17 __out DWORD64* pdw64ResumeOffset
18 );
19static HRESULT GetResourceMetadata(
20 __in HINTERNET hSession,
21 __inout_z LPWSTR* psczUrl,
22 __in_z_opt LPCWSTR wzUser,
23 __in_z_opt LPCWSTR wzPassword,
24 __in_opt DOWNLOAD_AUTHENTICATION_CALLBACK* pAuthenticate,
25 __out DWORD64* pdw64ResourceSize,
26 __out FILETIME* pftResourceCreated
27 );
28static HRESULT DownloadResource(
29 __in HINTERNET hSession,
30 __inout_z LPWSTR* psczUrl,
31 __in_z_opt LPCWSTR wzUser,
32 __in_z_opt LPCWSTR wzPassword,
33 __in_z LPCWSTR wzDestinationPath,
34 __in DWORD64 dw64AuthoredResourceLength,
35 __in DWORD64 dw64ResourceLength,
36 __in DWORD64 dw64ResumeOffset,
37 __in HANDLE hResumeFile,
38 __in_opt DOWNLOAD_CACHE_CALLBACK* pCache,
39 __in_opt DOWNLOAD_AUTHENTICATION_CALLBACK* pAuthenticate
40 );
41static HRESULT AllocateRangeRequestHeader(
42 __in DWORD64 dw64ResumeOffset,
43 __in DWORD64 dw64ResourceLength,
44 __deref_out_z LPWSTR* psczHeader
45 );
46static HRESULT WriteToFile(
47 __in HINTERNET hUrl,
48 __in HANDLE hPayloadFile,
49 __inout DWORD64* pdw64ResumeOffset,
50 __in HANDLE hResumeFile,
51 __in DWORD64 dw64ResourceLength,
52 __in LPBYTE pbData,
53 __in DWORD cbData,
54 __in_opt DOWNLOAD_CACHE_CALLBACK* pCallback
55 );
56static HRESULT UpdateResumeOffset(
57 __inout DWORD64* pdw64ResumeOffset,
58 __in HANDLE hResumeFile,
59 __in DWORD cbData
60 );
61static HRESULT MakeRequest(
62 __in HINTERNET hSession,
63 __inout_z LPWSTR* psczSourceUrl,
64 __in_z_opt LPCWSTR wzMethod,
65 __in_z_opt LPCWSTR wzHeaders,
66 __in_z_opt LPCWSTR wzUser,
67 __in_z_opt LPCWSTR wzPassword,
68 __in_opt DOWNLOAD_AUTHENTICATION_CALLBACK* pAuthenticate,
69 __out HINTERNET* phConnect,
70 __out HINTERNET* phUrl,
71 __out BOOL* pfRangeRequestsAccepted
72 );
73static HRESULT OpenRequest(
74 __in HINTERNET hConnect,
75 __in_z_opt LPCWSTR wzMethod,
76 __in INTERNET_SCHEME scheme,
77 __in_z LPCWSTR wzResource,
78 __in_z_opt LPCWSTR wzQueryString,
79 __in_z_opt LPCWSTR wzHeader,
80 __out HINTERNET* phUrl
81 );
82static HRESULT SendRequest(
83 __in HINTERNET hUrl,
84 __inout_z LPWSTR* psczUrl,
85 __in_opt DOWNLOAD_AUTHENTICATION_CALLBACK* pAuthenticate,
86 __out BOOL* pfRetry,
87 __out BOOL* pfRangesAccepted
88 );
89static HRESULT AuthenticationRequired(
90 __in HINTERNET hUrl,
91 __in long lHttpCode,
92 __in_opt DOWNLOAD_AUTHENTICATION_CALLBACK* pAuthenticate,
93 __out BOOL* pfRetrySend,
94 __out BOOL* pfRetry
95 );
96static HRESULT DownloadGetResumePath(
97 __in_z LPCWSTR wzPayloadWorkingPath,
98 __deref_out_z LPWSTR* psczResumePath
99 );
100static HRESULT DownloadSendProgressCallback(
101 __in DOWNLOAD_CACHE_CALLBACK* pCallback,
102 __in DWORD64 dw64Progress,
103 __in DWORD64 dw64Total,
104 __in HANDLE hDestinationFile
105 );
106// function definitions
107
108extern "C" HRESULT DAPI DownloadUrl(
109 __in DOWNLOAD_SOURCE* pDownloadSource,
110 __in DWORD64 dw64AuthoredDownloadSize,
111 __in LPCWSTR wzDestinationPath,
112 __in_opt DOWNLOAD_CACHE_CALLBACK* pCache,
113 __in_opt DOWNLOAD_AUTHENTICATION_CALLBACK* pAuthenticate
114 )
115{
116 HRESULT hr = S_OK;
117 LPWSTR sczUrl = NULL;
118 HINTERNET hSession = NULL;
119 DWORD dwTimeout = 0;
120 LPWSTR sczResumePath = NULL;
121 HANDLE hResumeFile = INVALID_HANDLE_VALUE;
122 DWORD64 dw64ResumeOffset = 0;
123 DWORD64 dw64Size = 0;
124 FILETIME ftCreated = { };
125
126 // Copy the download source into a working variable to handle redirects then
127 // open the internet session.
128 hr = StrAllocString(&sczUrl, pDownloadSource->sczUrl, 0);
129 ExitOnFailure(hr, "Failed to copy download source URL.");
130
131 hSession = ::InternetOpenW(L"Burn", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0);
132 ExitOnNullWithLastError(hSession, hr, "Failed to open internet session");
133
134 // Make a best effort to set the download timeouts to 2 minutes or whatever policy says.
135 PolcReadNumber(POLICY_BURN_REGISTRY_PATH, L"DownloadTimeout", 2 * 60, &dwTimeout);
136 if (0 < dwTimeout)
137 {
138 dwTimeout *= 1000; // convert to milliseconds.
139 ::InternetSetOptionW(hSession, INTERNET_OPTION_CONNECT_TIMEOUT, &dwTimeout, sizeof(dwTimeout));
140 ::InternetSetOptionW(hSession, INTERNET_OPTION_RECEIVE_TIMEOUT, &dwTimeout, sizeof(dwTimeout));
141 ::InternetSetOptionW(hSession, INTERNET_OPTION_SEND_TIMEOUT, &dwTimeout, sizeof(dwTimeout));
142 }
143
144 // Get the resource size and creation time from the internet.
145 hr = GetResourceMetadata(hSession, &sczUrl, pDownloadSource->sczUser, pDownloadSource->sczPassword, pAuthenticate, &dw64Size, &ftCreated);
146 ExitOnFailure(hr, "Failed to get size and time for URL: %ls", sczUrl);
147
148 // Ignore failure to initialize resume because we will fall back to full download then
149 // download.
150 InitializeResume(wzDestinationPath, &sczResumePath, &hResumeFile, &dw64ResumeOffset);
151
152 hr = DownloadResource(hSession, &sczUrl, pDownloadSource->sczUser, pDownloadSource->sczPassword, wzDestinationPath, dw64AuthoredDownloadSize, dw64Size, dw64ResumeOffset, hResumeFile, pCache, pAuthenticate);
153 ExitOnFailure(hr, "Failed to download URL: %ls", sczUrl);
154
155 // Cleanup the resume file because we successfully downloaded the whole file.
156 if (sczResumePath && *sczResumePath)
157 {
158 ::DeleteFileW(sczResumePath);
159 }
160
161LExit:
162 ReleaseFileHandle(hResumeFile);
163 ReleaseStr(sczResumePath);
164 ReleaseInternet(hSession);
165 ReleaseStr(sczUrl);
166
167 return hr;
168}
169
170
171// internal helper functions
172
173static HRESULT InitializeResume(
174 __in LPCWSTR wzDestinationPath,
175 __out LPWSTR* psczResumePath,
176 __out HANDLE* phResumeFile,
177 __out DWORD64* pdw64ResumeOffset
178 )
179{
180 HRESULT hr = S_OK;
181 HANDLE hResumeFile = INVALID_HANDLE_VALUE;
182 DWORD cbTotalReadResumeData = 0;
183 DWORD cbReadData = 0;
184
185 *pdw64ResumeOffset = 0;
186
187 hr = DownloadGetResumePath(wzDestinationPath, psczResumePath);
188 ExitOnFailure(hr, "Failed to calculate resume path from working path: %ls", wzDestinationPath);
189
190 hResumeFile = ::CreateFileW(*psczResumePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_DELETE, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
191 if (INVALID_HANDLE_VALUE == hResumeFile)
192 {
193 ExitWithLastError(hr, "Failed to create resume file: %ls", *psczResumePath);
194 }
195
196 do
197 {
198 if (!::ReadFile(hResumeFile, reinterpret_cast<BYTE*>(pdw64ResumeOffset) + cbTotalReadResumeData, sizeof(DWORD64) - cbTotalReadResumeData, &cbReadData, NULL))
199 {
200 ExitWithLastError(hr, "Failed to read resume file: %ls", *psczResumePath);
201 }
202 cbTotalReadResumeData += cbReadData;
203 } while (cbReadData && sizeof(DWORD64) > cbTotalReadResumeData);
204
205 // Start over if we couldn't get a resume offset.
206 if (cbTotalReadResumeData != sizeof(DWORD64))
207 {
208 *pdw64ResumeOffset = 0;
209 }
210
211 *phResumeFile = hResumeFile;
212 hResumeFile = INVALID_HANDLE_VALUE;
213
214LExit:
215 ReleaseFileHandle(hResumeFile);
216 return hr;
217}
218
219static HRESULT GetResourceMetadata(
220 __in HINTERNET hSession,
221 __inout_z LPWSTR* psczUrl,
222 __in_z_opt LPCWSTR wzUser,
223 __in_z_opt LPCWSTR wzPassword,
224 __in_opt DOWNLOAD_AUTHENTICATION_CALLBACK* pAuthenticate,
225 __out DWORD64* pdw64ResourceSize,
226 __out FILETIME* pftResourceCreated
227 )
228{
229 HRESULT hr = S_OK;
230 BOOL fRangeRequestsAccepted = TRUE;
231 HINTERNET hConnect = NULL;
232 HINTERNET hUrl = NULL;
233 LONGLONG llLength = 0;
234
235 hr = MakeRequest(hSession, psczUrl, L"HEAD", NULL, wzUser, wzPassword, pAuthenticate, &hConnect, &hUrl, &fRangeRequestsAccepted);
236 ExitOnFailure(hr, "Failed to connect to URL: %ls", *psczUrl);
237
238 hr = InternetGetSizeByHandle(hUrl, &llLength);
239 if (FAILED(hr))
240 {
241 llLength = 0;
242 hr = S_OK;
243 }
244
245 *pdw64ResourceSize = llLength;
246
247 // Get the last modified time from the server, we'll use that as our downloaded time here. If
248 // the server time isn't available then use the local system time.
249 hr = InternetGetCreateTimeByHandle(hUrl, pftResourceCreated);
250 if (FAILED(hr))
251 {
252 ::GetSystemTimeAsFileTime(pftResourceCreated);
253 hr = S_OK;
254 }
255
256LExit:
257 ReleaseInternet(hUrl);
258 ReleaseInternet(hConnect);
259 return hr;
260}
261
262static HRESULT DownloadResource(
263 __in HINTERNET hSession,
264 __inout_z LPWSTR* psczUrl,
265 __in_z_opt LPCWSTR wzUser,
266 __in_z_opt LPCWSTR wzPassword,
267 __in_z LPCWSTR wzDestinationPath,
268 __in DWORD64 dw64AuthoredResourceLength,
269 __in DWORD64 dw64ResourceLength,
270 __in DWORD64 dw64ResumeOffset,
271 __in HANDLE hResumeFile,
272 __in_opt DOWNLOAD_CACHE_CALLBACK* pCache,
273 __in_opt DOWNLOAD_AUTHENTICATION_CALLBACK* pAuthenticate
274 )
275{
276 HRESULT hr = S_OK;
277 HANDLE hPayloadFile = INVALID_HANDLE_VALUE;
278 DWORD cbMaxData = 64 * 1024; // 64 KB
279 BYTE* pbData = NULL;
280 BOOL fRangeRequestsAccepted = TRUE;
281 LPWSTR sczRangeRequestHeader = NULL;
282 HINTERNET hConnect = NULL;
283 HINTERNET hUrl = NULL;
284 LONGLONG llLength = 0;
285
286 hPayloadFile = ::CreateFileW(wzDestinationPath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_DELETE, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
287 if (INVALID_HANDLE_VALUE == hPayloadFile)
288 {
289 ExitWithLastError(hr, "Failed to create download destination file: %ls", wzDestinationPath);
290 }
291
292 // Allocate a memory block on a page boundary in case we want to do optimal writing.
293 pbData = static_cast<BYTE*>(::VirtualAlloc(NULL, cbMaxData, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
294 ExitOnNullWithLastError(pbData, hr, "Failed to allocate buffer to download files into.");
295
296 // Let's try downloading the file assuming that range requests are accepted. If range requests
297 // are not supported we'll have to start over and accept the fact that we only get one shot
298 // downloading the file however big it is. Hopefully, not more than 2 GB since wininet doesn't
299 // like files that big.
300 while (fRangeRequestsAccepted && (0 == dw64ResourceLength || dw64ResumeOffset < dw64ResourceLength))
301 {
302 hr = AllocateRangeRequestHeader(dw64ResumeOffset, 0 == dw64ResourceLength ? dw64AuthoredResourceLength : dw64ResourceLength, &sczRangeRequestHeader);
303 ExitOnFailure(hr, "Failed to allocate range request header.");
304
305 ReleaseNullInternet(hConnect);
306 ReleaseNullInternet(hUrl);
307
308 hr = MakeRequest(hSession, psczUrl, L"GET", sczRangeRequestHeader, wzUser, wzPassword, pAuthenticate, &hConnect, &hUrl, &fRangeRequestsAccepted);
309 ExitOnFailure(hr, "Failed to request URL for download: %ls", *psczUrl);
310
311 // If we didn't get the size of the resource from the initial "HEAD" request
312 // then let's try to get the size from this "GET" request.
313 if (0 == dw64ResourceLength)
314 {
315 hr = InternetGetSizeByHandle(hUrl, &llLength);
316 if (SUCCEEDED(hr))
317 {
318 dw64ResourceLength = llLength;
319 }
320 else // server didn't tell us the resource length.
321 {
322 // Fallback to the authored size of the resource. However, since we
323 // don't really know the size on the server, don't try to use
324 // range requests either.
325 dw64ResourceLength = dw64AuthoredResourceLength;
326 fRangeRequestsAccepted = FALSE;
327 }
328 }
329
330 // If we just tried to do a range request and found out that it isn't supported, start over.
331 if (!fRangeRequestsAccepted)
332 {
333 // TODO: log a message that the server did not accept range requests.
334 dw64ResumeOffset = 0;
335 }
336
337 hr = WriteToFile(hUrl, hPayloadFile, &dw64ResumeOffset, hResumeFile, dw64ResourceLength, pbData, cbMaxData, pCache);
338 ExitOnFailure(hr, "Failed while reading from internet and writing to: %ls", wzDestinationPath);
339 }
340
341LExit:
342 ReleaseInternet(hUrl);
343 ReleaseInternet(hConnect);
344 ReleaseStr(sczRangeRequestHeader);
345 if (pbData)
346 {
347 ::VirtualFree(pbData, 0, MEM_RELEASE);
348 }
349 ReleaseFileHandle(hPayloadFile);
350
351 return hr;
352}
353
354static HRESULT AllocateRangeRequestHeader(
355 __in DWORD64 dw64ResumeOffset,
356 __in DWORD64 dw64ResourceLength,
357 __deref_out_z LPWSTR* psczHeader
358 )
359{
360 HRESULT hr = S_OK;
361
362 // If the remaining length is less that 2GB we'll be able to ask for everything.
363 DWORD64 dw64RemainingLength = dw64ResourceLength - dw64ResumeOffset;
364 if (DOWNLOAD_ENGINE_TWO_GIGABYTES > dw64RemainingLength)
365 {
366 // If we have a resume offset, let's download everything from there. Otherwise, we'll
367 // just get everything with no headers in the way.
368 if (0 < dw64ResumeOffset)
369 {
370 hr = StrAllocFormatted(psczHeader, L"Range: bytes=%I64u-", dw64ResumeOffset);
371 ExitOnFailure(hr, "Failed to add range read header.");
372 }
373 else
374 {
375 ReleaseNullStr(*psczHeader);
376 }
377 }
378 else // we'll have to download in chunks.
379 {
380 hr = StrAllocFormatted(psczHeader, L"Range: bytes=%I64u-%I64u", dw64ResumeOffset, dw64ResumeOffset + dw64RemainingLength - 1);
381 ExitOnFailure(hr, "Failed to add range read header.");
382 }
383
384LExit:
385 return hr;
386}
387
388static HRESULT WriteToFile(
389 __in HINTERNET hUrl,
390 __in HANDLE hPayloadFile,
391 __inout DWORD64* pdw64ResumeOffset,
392 __in HANDLE hResumeFile,
393 __in DWORD64 dw64ResourceLength,
394 __in LPBYTE pbData,
395 __in DWORD cbData,
396 __in_opt DOWNLOAD_CACHE_CALLBACK* pCallback
397 )
398{
399 HRESULT hr = S_OK;
400 DWORD cbReadData = 0;
401
402 hr = FileSetPointer(hPayloadFile, *pdw64ResumeOffset, NULL, FILE_BEGIN);
403 ExitOnFailure(hr, "Failed to seek to start point in file.");
404
405 do
406 {
407 // Read bits from the internet.
408 if (!::InternetReadFile(hUrl, static_cast<void*>(pbData), cbData, &cbReadData))
409 {
410 ExitWithLastError(hr, "Failed while reading from internet.");
411 }
412
413 // Write bits to disk (if there are any).
414 if (cbReadData)
415 {
416 DWORD cbTotalWritten = 0;
417 DWORD cbWritten = 0;
418 do
419 {
420 if (!::WriteFile(hPayloadFile, pbData + cbTotalWritten, cbReadData - cbTotalWritten, &cbWritten, NULL))
421 {
422 ExitWithLastError(hr, "Failed to write data from internet.");
423 }
424
425 cbTotalWritten += cbWritten;
426 } while (cbWritten && cbTotalWritten < cbReadData);
427
428 // Ignore failure from updating resume file as this doesn't mean the download cannot succeed.
429 UpdateResumeOffset(pdw64ResumeOffset, hResumeFile, cbTotalWritten);
430
431 if (pCallback && pCallback->pfnProgress)
432 {
433 hr = DownloadSendProgressCallback(pCallback, *pdw64ResumeOffset, dw64ResourceLength, hPayloadFile);
434 ExitOnFailure(hr, "UX aborted on cache progress.");
435 }
436 }
437 } while (cbReadData);
438
439LExit:
440 return hr;
441}
442
443static HRESULT UpdateResumeOffset(
444 __inout DWORD64* pdw64ResumeOffset,
445 __in HANDLE hResumeFile,
446 __in DWORD cbData
447 )
448{
449 HRESULT hr = S_OK;
450
451 *pdw64ResumeOffset += cbData;
452
453 if (INVALID_HANDLE_VALUE != hResumeFile)
454 {
455 DWORD cbTotalWrittenResumeData = 0;
456 DWORD cbWrittenResumeData = 0;
457
458 hr = FileSetPointer(hResumeFile, 0, NULL, FILE_BEGIN);
459 ExitOnFailure(hr, "Failed to seek to start point in file.");
460
461 do
462 {
463 // Ignore failure to write to the resume file as that should not prevent the download from happening.
464 if (!::WriteFile(hResumeFile, pdw64ResumeOffset + cbTotalWrittenResumeData, sizeof(DWORD64) - cbTotalWrittenResumeData, &cbWrittenResumeData, NULL))
465 {
466 ExitOnFailure(hr, "Failed to seek to write to file.");
467 }
468
469 cbTotalWrittenResumeData += cbWrittenResumeData;
470 } while (cbWrittenResumeData && sizeof(DWORD64) > cbTotalWrittenResumeData);
471 }
472
473LExit:
474 return hr;
475}
476
477static HRESULT MakeRequest(
478 __in HINTERNET hSession,
479 __inout_z LPWSTR* psczSourceUrl,
480 __in_z_opt LPCWSTR wzMethod,
481 __in_z_opt LPCWSTR wzHeaders,
482 __in_z_opt LPCWSTR wzUser,
483 __in_z_opt LPCWSTR wzPassword,
484 __in_opt DOWNLOAD_AUTHENTICATION_CALLBACK* pAuthenticate,
485 __out HINTERNET* phConnect,
486 __out HINTERNET* phUrl,
487 __out BOOL* pfRangeRequestsAccepted
488 )
489{
490 HRESULT hr = S_OK;
491 HINTERNET hConnect = NULL;
492 HINTERNET hUrl = NULL;
493 URI_INFO uri = { };
494
495 // Try to open the URL.
496 BOOL fRetry;
497 do
498 {
499 fRetry = FALSE;
500
501 // If the URL was opened close it, so we can reopen it again.
502 ReleaseInternet(hUrl);
503 ReleaseInternet(hConnect);
504
505 // Open the url.
506 hr = UriCrackEx(*psczSourceUrl, &uri);
507 ExitOnFailure(hr, "Failed to break URL into server and resource parts.");
508
509 hConnect = ::InternetConnectW(hSession, uri.sczHostName, uri.port, (wzUser && *wzUser) ? wzUser : uri.sczUser, (wzPassword && *wzPassword) ? wzPassword : uri.sczPassword, INTERNET_SCHEME_FTP == uri.scheme ? INTERNET_SERVICE_FTP : INTERNET_SERVICE_HTTP, 0, 0);
510 ExitOnNullWithLastError(hConnect, hr, "Failed to connect to URL: %ls", *psczSourceUrl);
511
512 // Best effort set the proxy username and password, if they were provided.
513 if ((wzUser && *wzUser) && (wzPassword && *wzPassword))
514 {
515 if (::InternetSetOptionW(hConnect, INTERNET_OPTION_PROXY_USERNAME, (LPVOID)wzUser, lstrlenW(wzUser)))
516 {
517 ::InternetSetOptionW(hConnect, INTERNET_OPTION_PROXY_PASSWORD, (LPVOID)wzPassword, lstrlenW(wzPassword));
518 }
519 }
520
521 hr = OpenRequest(hConnect, wzMethod, uri.scheme, uri.sczPath, uri.sczQueryString, wzHeaders, &hUrl);
522 ExitOnFailure(hr, "Failed to open internet URL: %ls", *psczSourceUrl);
523
524 hr = SendRequest(hUrl, psczSourceUrl, pAuthenticate, &fRetry, pfRangeRequestsAccepted);
525 ExitOnFailure(hr, "Failed to send request to URL: %ls", *psczSourceUrl);
526 } while (fRetry);
527
528 // Okay, we're all ready to start downloading. Update the connection information.
529 *phConnect = hConnect;
530 hConnect = NULL;
531 *phUrl = hUrl;
532 hUrl = NULL;
533
534LExit:
535 UriInfoUninitialize(&uri);
536 ReleaseInternet(hUrl);
537 ReleaseInternet(hConnect);
538
539 return hr;
540}
541
542static HRESULT OpenRequest(
543 __in HINTERNET hConnect,
544 __in_z_opt LPCWSTR wzMethod,
545 __in INTERNET_SCHEME scheme,
546 __in_z LPCWSTR wzResource,
547 __in_z_opt LPCWSTR wzQueryString,
548 __in_z_opt LPCWSTR wzHeader,
549 __out HINTERNET* phUrl
550 )
551{
552 HRESULT hr = S_OK;
553 DWORD dwRequestFlags = INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_NO_CACHE_WRITE | INTERNET_FLAG_NO_UI | INTERNET_FLAG_RELOAD;
554 LPWSTR sczResource = NULL;
555 HINTERNET hUrl = NULL;
556
557 if (INTERNET_SCHEME_HTTPS == scheme)
558 {
559 dwRequestFlags |= INTERNET_FLAG_SECURE;
560 }
561
562 // Allocate the resource name.
563 hr = StrAllocString(&sczResource, wzResource, 0);
564 ExitOnFailure(hr, "Failed to allocate string for resource URI.");
565
566 if (wzQueryString && *wzQueryString)
567 {
568 hr = StrAllocConcat(&sczResource, wzQueryString, 0);
569 ExitOnFailure(hr, "Failed to append query strong to resource from URI.");
570 }
571
572 // Open the request and add the header if provided.
573 hUrl = ::HttpOpenRequestW(hConnect, wzMethod, sczResource, NULL, NULL, DOWNLOAD_ENGINE_ACCEPT_TYPES, dwRequestFlags, NULL);
574 ExitOnNullWithLastError(hUrl, hr, "Failed to open internet request.");
575
576 if (wzHeader && *wzHeader)
577 {
578 if (!::HttpAddRequestHeadersW(hUrl, wzHeader, static_cast<DWORD>(-1), HTTP_ADDREQ_FLAG_COALESCE))
579 {
580 ExitWithLastError(hr, "Failed to add header to HTTP request.");
581 }
582 }
583
584 *phUrl = hUrl;
585 hUrl = NULL;
586
587LExit:
588 ReleaseInternet(hUrl);
589 ReleaseStr(sczResource);
590 return hr;
591}
592
593static HRESULT SendRequest(
594 __in HINTERNET hUrl,
595 __inout_z LPWSTR* psczUrl,
596 __in_opt DOWNLOAD_AUTHENTICATION_CALLBACK* pAuthenticate,
597 __out BOOL* pfRetry,
598 __out BOOL* pfRangesAccepted
599 )
600{
601 HRESULT hr = S_OK;
602 BOOL fRetrySend = FALSE;
603 LONG lCode = 0;
604
605 do
606 {
607 fRetrySend = FALSE;
608
609 if (!::HttpSendRequestW(hUrl, NULL, 0, NULL, 0))
610 {
611 hr = HRESULT_FROM_WIN32(::GetLastError()); // remember the error that occurred and log it.
612 LogErrorString(hr, "Failed to send request to URL: %ls, trying to process HTTP status code anyway.", *psczUrl);
613
614 // Try to get the HTTP status code and, if good, handle via the switch statement below but if it
615 // fails return the error code from the send request above as the result of the function.
616 HRESULT hrQueryStatusCode = InternetQueryInfoNumber(hUrl, HTTP_QUERY_STATUS_CODE, &lCode);
617 ExitOnFailure(hrQueryStatusCode, "Failed to get HTTP status code for failed request to URL: %ls", *psczUrl);
618 }
619 else // get the http status code.
620 {
621 hr = InternetQueryInfoNumber(hUrl, HTTP_QUERY_STATUS_CODE, &lCode);
622 ExitOnFailure(hr, "Failed to get HTTP status code for request to URL: %ls", *psczUrl);
623 }
624
625 switch (lCode)
626 {
627 case 200: // OK but range requests don't work.
628 *pfRangesAccepted = FALSE;
629 hr = S_OK;
630 break;
631
632 case 206: // Partial content means that range requests work!
633 *pfRangesAccepted = TRUE;
634 hr = S_OK;
635 break;
636
637 // redirection cases
638 case 301: __fallthrough; // file moved
639 case 302: __fallthrough; // temporary
640 case 303: // redirect method
641 hr = InternetQueryInfoString(hUrl, HTTP_QUERY_CONTENT_LOCATION, psczUrl);
642 ExitOnFailure(hr, "Failed to get redirect url: %ls", *psczUrl);
643
644 *pfRetry = TRUE;
645 break;
646
647 // error cases
648 case 400: // bad request
649 hr = HRESULT_FROM_WIN32(ERROR_BAD_PATHNAME);
650 break;
651
652 case 401: __fallthrough; // unauthorized
653 case 407: __fallthrough; // proxy unauthorized
654 hr = AuthenticationRequired(hUrl, lCode, pAuthenticate, &fRetrySend, pfRetry);
655 break;
656
657 case 403: // forbidden
658 hr = HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED);
659 break;
660
661 case 404: // file not found
662 case 410: // gone
663 hr = HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND);
664 break;
665
666 case 405: // method not allowed
667 hr = HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED);
668 break;
669
670 case 408: __fallthrough; // request timedout
671 case 504: // gateway timeout
672 hr = HRESULT_FROM_WIN32(WAIT_TIMEOUT);
673 break;
674
675 case 414: // request URI too long
676 hr = CO_E_PATHTOOLONG;
677 break;
678
679 case 502: __fallthrough; // server (through a gateway) was not found
680 case 503: // server unavailable
681 hr = HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND);
682 break;
683
684 case 418: // I'm a teapot.
685 default:
686 // If the request failed and the HTTP status code was invalid (but wininet gave us a number anyway)
687 // do not overwrite the error code from the failed request. Otherwise, the error was unexpected.
688 if (SUCCEEDED(hr))
689 {
690 hr = E_UNEXPECTED;
691 }
692
693 LogErrorString(hr, "Unknown HTTP status code %d, returned from URL: %ls", lCode, *psczUrl);
694 break;
695 }
696 } while (fRetrySend);
697
698LExit:
699 return hr;
700}
701
702static HRESULT AuthenticationRequired(
703 __in HINTERNET hUrl,
704 __in long lHttpCode,
705 __in_opt DOWNLOAD_AUTHENTICATION_CALLBACK* pAuthenticate,
706 __out BOOL* pfRetrySend,
707 __out BOOL* pfRetry
708 )
709{
710 Assert(401 == lHttpCode || 407 == lHttpCode);
711
712 HRESULT hr = HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED);
713 *pfRetrySend = FALSE;
714 *pfRetry = FALSE;
715
716 if (pAuthenticate && pAuthenticate->pfnAuthenticate)
717 {
718 hr = (*pAuthenticate->pfnAuthenticate)(pAuthenticate->pv, hUrl, lHttpCode, pfRetrySend, pfRetry);
719 }
720
721 return hr;
722}
723
724
725static HRESULT DownloadGetResumePath(
726 __in_z LPCWSTR wzPayloadWorkingPath,
727 __deref_out_z LPWSTR* psczResumePath
728 )
729{
730 HRESULT hr = S_OK;
731
732 hr = StrAllocFormatted(psczResumePath, L"%ls.R", wzPayloadWorkingPath);
733 ExitOnFailure(hr, "Failed to create resume path.");
734
735LExit:
736 return hr;
737}
738
739static HRESULT DownloadSendProgressCallback(
740 __in DOWNLOAD_CACHE_CALLBACK* pCallback,
741 __in DWORD64 dw64Progress,
742 __in DWORD64 dw64Total,
743 __in HANDLE hDestinationFile
744 )
745{
746 static LARGE_INTEGER LARGE_INTEGER_ZERO = { };
747
748 HRESULT hr = S_OK;
749 DWORD dwResult = PROGRESS_CONTINUE;
750 LARGE_INTEGER liTotalSize = { };
751 LARGE_INTEGER liTotalTransferred = { };
752
753 if (pCallback->pfnProgress)
754 {
755 liTotalSize.QuadPart = dw64Total;
756 liTotalTransferred.QuadPart = dw64Progress;
757
758 dwResult = (*pCallback->pfnProgress)(liTotalSize, liTotalTransferred, LARGE_INTEGER_ZERO, LARGE_INTEGER_ZERO, 1, CALLBACK_CHUNK_FINISHED, INVALID_HANDLE_VALUE, hDestinationFile, pCallback->pv);
759 switch (dwResult)
760 {
761 case PROGRESS_CONTINUE:
762 hr = S_OK;
763 break;
764
765 case PROGRESS_CANCEL: __fallthrough; // TODO: should cancel and stop be treated differently?
766 case PROGRESS_STOP:
767 hr = HRESULT_FROM_WIN32(ERROR_INSTALL_USEREXIT);
768 ExitOnRootFailure(hr, "UX aborted on download progress.");
769
770 case PROGRESS_QUIET: // Not actually an error, just an indication to the caller to stop requesting progress.
771 pCallback->pfnProgress = NULL;
772 hr = S_OK;
773 break;
774
775 default:
776 hr = E_UNEXPECTED;
777 ExitOnRootFailure(hr, "Invalid return code from progress routine.");
778 }
779 }
780
781LExit:
782 return hr;
783}