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