diff options
| author | Sean Hall <r.sean.hall@gmail.com> | 2022-02-18 17:56:31 -0600 |
|---|---|---|
| committer | Sean Hall <r.sean.hall@gmail.com> | 2022-02-19 11:53:06 -0700 |
| commit | 28c0a27ddf03dcf07a11c291699428a32f381fbc (patch) | |
| tree | 443b0cf0413d3bff3d70bde53d702372cea5caf6 /src | |
| parent | 7750404222a7c5bb6543dd246c2cce0f7c097d8d (diff) | |
| download | wix-28c0a27ddf03dcf07a11c291699428a32f381fbc.tar.gz wix-28c0a27ddf03dcf07a11c291699428a32f381fbc.tar.bz2 wix-28c0a27ddf03dcf07a11c291699428a32f381fbc.zip | |
Handle missing content length with range request and empty files.
Add test for server without range request support.
Diffstat (limited to 'src')
5 files changed, 161 insertions, 13 deletions
diff --git a/src/libs/dutil/WixToolset.DUtil/dlutil.cpp b/src/libs/dutil/WixToolset.DUtil/dlutil.cpp index f6b793b2..9c704ee8 100644 --- a/src/libs/dutil/WixToolset.DUtil/dlutil.cpp +++ b/src/libs/dutil/WixToolset.DUtil/dlutil.cpp | |||
| @@ -295,7 +295,10 @@ static HRESULT DownloadResource( | |||
| 295 | HANDLE hPayloadFile = INVALID_HANDLE_VALUE; | 295 | HANDLE hPayloadFile = INVALID_HANDLE_VALUE; |
| 296 | DWORD cbMaxData = 64 * 1024; // 64 KB | 296 | DWORD cbMaxData = 64 * 1024; // 64 KB |
| 297 | BYTE* pbData = NULL; | 297 | BYTE* pbData = NULL; |
| 298 | BOOL fRangeRequestsAccepted = TRUE; | 298 | BOOL fUseRangeRequest = TRUE; |
| 299 | BOOL fRangeRequestsAccepted = FALSE; | ||
| 300 | BOOL fRequestedRangeRequest = FALSE; | ||
| 301 | BOOL fInvalidRangeRequestResponse = FALSE; | ||
| 299 | LPWSTR sczRangeRequestHeader = NULL; | 302 | LPWSTR sczRangeRequestHeader = NULL; |
| 300 | HINTERNET hConnect = NULL; | 303 | HINTERNET hConnect = NULL; |
| 301 | HINTERNET hUrl = NULL; | 304 | HINTERNET hUrl = NULL; |
| @@ -315,10 +318,19 @@ static HRESULT DownloadResource( | |||
| 315 | // are not supported we'll have to start over and accept the fact that we only get one shot | 318 | // are not supported we'll have to start over and accept the fact that we only get one shot |
| 316 | // downloading the file however big it is. Hopefully, not more than 2 GB since wininet doesn't | 319 | // downloading the file however big it is. Hopefully, not more than 2 GB since wininet doesn't |
| 317 | // like files that big. | 320 | // like files that big. |
| 318 | while (fRangeRequestsAccepted && (0 == dw64ResourceLength || dw64ResumeOffset < dw64ResourceLength)) | 321 | for (;;) |
| 319 | { | 322 | { |
| 320 | hr = AllocateRangeRequestHeader(dw64ResumeOffset, 0 == dw64ResourceLength ? dw64AuthoredResourceLength : dw64ResourceLength, &sczRangeRequestHeader); | 323 | fInvalidRangeRequestResponse = FALSE; |
| 321 | DlExitOnFailure(hr, "Failed to allocate range request header."); | 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 | } | ||
| 322 | 334 | ||
| 323 | ReleaseNullInternet(hConnect); | 335 | ReleaseNullInternet(hConnect); |
| 324 | ReleaseNullInternet(hUrl); | 336 | ReleaseNullInternet(hUrl); |
| @@ -326,6 +338,13 @@ static HRESULT DownloadResource( | |||
| 326 | hr = MakeRequest(hSession, psczUrl, L"GET", sczRangeRequestHeader, wzUser, wzPassword, pAuthenticate, &hConnect, &hUrl, &fRangeRequestsAccepted); | 338 | hr = MakeRequest(hSession, psczUrl, L"GET", sczRangeRequestHeader, wzUser, wzPassword, pAuthenticate, &hConnect, &hUrl, &fRangeRequestsAccepted); |
| 327 | DlExitOnFailure(hr, "Failed to request URL for download: %ls", *psczUrl); | 339 | DlExitOnFailure(hr, "Failed to request URL for download: %ls", *psczUrl); |
| 328 | 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 | |||
| 329 | // If we didn't get the size of the resource from the initial "HEAD" request | 348 | // If we didn't get the size of the resource from the initial "HEAD" request |
| 330 | // then let's try to get the size from this "GET" request. | 349 | // then let's try to get the size from this "GET" request. |
| 331 | if (0 == dw64ResourceLength) | 350 | if (0 == dw64ResourceLength) |
| @@ -337,23 +356,36 @@ static HRESULT DownloadResource( | |||
| 337 | } | 356 | } |
| 338 | else // server didn't tell us the resource length. | 357 | else // server didn't tell us the resource length. |
| 339 | { | 358 | { |
| 359 | LogStringLine(REPORT_VERBOSE, "Content-Length not returned for URL: %ls", *psczUrl); | ||
| 360 | |||
| 340 | // Fallback to the authored size of the resource. However, since we | 361 | // Fallback to the authored size of the resource. However, since we |
| 341 | // don't really know the size on the server, don't try to use | 362 | // don't really know the size on the server, don't try to use |
| 342 | // range requests either. | 363 | // range requests either. |
| 343 | dw64ResourceLength = dw64AuthoredResourceLength; | 364 | dw64ResourceLength = dw64AuthoredResourceLength; |
| 365 | fInvalidRangeRequestResponse = fRequestedRangeRequest; | ||
| 344 | fRangeRequestsAccepted = FALSE; | 366 | fRangeRequestsAccepted = FALSE; |
| 345 | } | 367 | } |
| 346 | } | 368 | } |
| 347 | 369 | ||
| 348 | // If we just tried to do a range request and found out that it isn't supported, start over. | 370 | // If we just tried to do a range request and found out that it isn't supported, ignore the offset. |
| 349 | if (!fRangeRequestsAccepted) | 371 | if (fRequestedRangeRequest && !fRangeRequestsAccepted) |
| 350 | { | 372 | { |
| 351 | // TODO: log a message that the server did not accept range requests. | ||
| 352 | dw64ResumeOffset = 0; | 373 | dw64ResumeOffset = 0; |
| 374 | fUseRangeRequest = FALSE; | ||
| 375 | } | ||
| 376 | |||
| 377 | if (fInvalidRangeRequestResponse) | ||
| 378 | { | ||
| 379 | continue; | ||
| 353 | } | 380 | } |
| 354 | 381 | ||
| 355 | hr = WriteToFile(hUrl, hPayloadFile, &dw64ResumeOffset, hResumeFile, dw64ResourceLength, pbData, cbMaxData, pCache); | 382 | hr = WriteToFile(hUrl, hPayloadFile, &dw64ResumeOffset, hResumeFile, dw64ResourceLength, pbData, cbMaxData, pCache); |
| 356 | DlExitOnFailure(hr, "Failed while reading from internet and writing to: %ls", wzDestinationPath); | 383 | DlExitOnFailure(hr, "Failed while reading from internet and writing to: %ls", wzDestinationPath); |
| 384 | |||
| 385 | if (!fUseRangeRequest || dw64ResumeOffset >= dw64ResourceLength) | ||
| 386 | { | ||
| 387 | break; | ||
| 388 | } | ||
| 357 | } | 389 | } |
| 358 | 390 | ||
| 359 | LExit: | 391 | LExit: |
diff --git a/src/test/burn/TestData/CacheTests/BundleC/BundleC.wxs b/src/test/burn/TestData/CacheTests/BundleC/BundleC.wxs index ca21cc6e..865f7578 100644 --- a/src/test/burn/TestData/CacheTests/BundleC/BundleC.wxs +++ b/src/test/burn/TestData/CacheTests/BundleC/BundleC.wxs | |||
| @@ -5,7 +5,7 @@ | |||
| 5 | <Fragment> | 5 | <Fragment> |
| 6 | <PackageGroup Id="BundlePackages"> | 6 | <PackageGroup Id="BundlePackages"> |
| 7 | <MsiPackage Id="PackageA" SourceFile="$(var.PackageA.TargetPath)"> | 7 | <MsiPackage Id="PackageA" SourceFile="$(var.PackageA.TargetPath)"> |
| 8 | <Payload SourceFile="fivegb.file" Compressed="no" /> | 8 | <Payload SourceFile="fivegb.file" Compressed="no" DownloadUrl="$(var.WebServerBaseUrl)BundleC/{2}" /> |
| 9 | </MsiPackage> | 9 | </MsiPackage> |
| 10 | </PackageGroup> | 10 | </PackageGroup> |
| 11 | </Fragment> | 11 | </Fragment> |
diff --git a/src/test/burn/WixToolsetTest.BurnE2E/CacheTests.cs b/src/test/burn/WixToolsetTest.BurnE2E/CacheTests.cs index 4a3ffa2e..ee61baff 100644 --- a/src/test/burn/WixToolsetTest.BurnE2E/CacheTests.cs +++ b/src/test/burn/WixToolsetTest.BurnE2E/CacheTests.cs | |||
| @@ -15,8 +15,7 @@ namespace WixToolsetTest.BurnE2E | |||
| 15 | { | 15 | { |
| 16 | public CacheTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { } | 16 | public CacheTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { } |
| 17 | 17 | ||
| 18 | [Fact] | 18 | private bool Is5GBFileAvailable() |
| 19 | public void CanCache5GBFile() | ||
| 20 | { | 19 | { |
| 21 | // Recreate the 5GB payload to avoid having to copy it to the VM to run the tests. | 20 | // Recreate the 5GB payload to avoid having to copy it to the VM to run the tests. |
| 22 | const long FiveGB = 5_368_709_120; | 21 | const long FiveGB = 5_368_709_120; |
| @@ -28,8 +27,8 @@ namespace WixToolsetTest.BurnE2E | |||
| 28 | var drive = new DriveInfo(targetFilePath.Substring(0, 1)); | 27 | var drive = new DriveInfo(targetFilePath.Substring(0, 1)); |
| 29 | if (drive.AvailableFreeSpace < FiveGB + OneGB) | 28 | if (drive.AvailableFreeSpace < FiveGB + OneGB) |
| 30 | { | 29 | { |
| 31 | Console.WriteLine("Skipping CanCache5GBFile() test because there is not enough disk space available to run the test."); | 30 | Console.WriteLine($"Skipping {this.TestContext.TestName} because there is not enough disk space available to run the test."); |
| 32 | return; | 31 | return false; |
| 33 | } | 32 | } |
| 34 | 33 | ||
| 35 | if (!File.Exists(targetFilePath)) | 34 | if (!File.Exists(targetFilePath)) |
| @@ -42,6 +41,17 @@ namespace WixToolsetTest.BurnE2E | |||
| 42 | testTool.Run(true); | 41 | testTool.Run(true); |
| 43 | } | 42 | } |
| 44 | 43 | ||
| 44 | return true; | ||
| 45 | } | ||
| 46 | |||
| 47 | [Fact] | ||
| 48 | public void CanCache5GBFile() | ||
| 49 | { | ||
| 50 | if (!this.Is5GBFileAvailable()) | ||
| 51 | { | ||
| 52 | return; | ||
| 53 | } | ||
| 54 | |||
| 45 | var packageA = this.CreatePackageInstaller("PackageA"); | 55 | var packageA = this.CreatePackageInstaller("PackageA"); |
| 46 | var bundleC = this.CreateBundleInstaller("BundleC"); | 56 | var bundleC = this.CreateBundleInstaller("BundleC"); |
| 47 | 57 | ||
| @@ -53,6 +63,69 @@ namespace WixToolsetTest.BurnE2E | |||
| 53 | packageA.VerifyInstalled(true); | 63 | packageA.VerifyInstalled(true); |
| 54 | } | 64 | } |
| 55 | 65 | ||
| 66 | private string Cache5GBFileFromDownload(bool disableRangeRequests) | ||
| 67 | { | ||
| 68 | if (!this.Is5GBFileAvailable()) | ||
| 69 | { | ||
| 70 | return null; | ||
| 71 | } | ||
| 72 | |||
| 73 | var packageA = this.CreatePackageInstaller("PackageA"); | ||
| 74 | var bundleC = this.CreateBundleInstaller("BundleC"); | ||
| 75 | var webServer = this.CreateWebServer(); | ||
| 76 | |||
| 77 | webServer.AddFiles(new Dictionary<string, string> | ||
| 78 | { | ||
| 79 | { "/BundleC/fivegb.file", Path.Combine(this.TestContext.TestDataFolder, "fivegb.file") }, | ||
| 80 | { "/BundleC/PackageA.msi", Path.Combine(this.TestContext.TestDataFolder, "PackageA.msi") }, | ||
| 81 | }); | ||
| 82 | webServer.DisableRangeRequests = disableRangeRequests; | ||
| 83 | webServer.Start(); | ||
| 84 | |||
| 85 | using var dfs = new DisposableFileSystem(); | ||
| 86 | var separateDirectory = dfs.GetFolder(true); | ||
| 87 | |||
| 88 | // Manually copy bundle to separate directory and then run from there so the non-compressed payloads have to be resolved. | ||
| 89 | var bundleCFileInfo = new FileInfo(bundleC.Bundle); | ||
| 90 | var bundleCCopiedPath = Path.Combine(separateDirectory, bundleCFileInfo.Name); | ||
| 91 | bundleCFileInfo.CopyTo(bundleCCopiedPath); | ||
| 92 | |||
| 93 | packageA.VerifyInstalled(false); | ||
| 94 | |||
| 95 | var installLogPath = bundleC.Install(bundleCCopiedPath); | ||
| 96 | bundleC.VerifyRegisteredAndInPackageCache(); | ||
| 97 | |||
| 98 | packageA.VerifyInstalled(true); | ||
| 99 | |||
| 100 | return installLogPath; | ||
| 101 | } | ||
| 102 | |||
| 103 | [Fact] | ||
| 104 | public void CanCache5GBFileFromDownloadWithRangeRequestSupport() | ||
| 105 | { | ||
| 106 | var logPath = this.Cache5GBFileFromDownload(false); | ||
| 107 | if (logPath == null) | ||
| 108 | { | ||
| 109 | return; | ||
| 110 | } | ||
| 111 | |||
| 112 | Assert.False(LogVerifier.MessageInLogFile(logPath, "Range request not supported for URL: http://localhost:9999/e2e/BundleC/fivegb.file")); | ||
| 113 | Assert.True(LogVerifier.MessageInLogFile(logPath, "Content-Length not returned for URL: http://localhost:9999/e2e/BundleC/fivegb.file")); | ||
| 114 | } | ||
| 115 | |||
| 116 | [Fact] | ||
| 117 | public void CanCache5GBFileFromDownloadWithoutRangeRequestSupport() | ||
| 118 | { | ||
| 119 | var logPath = this.Cache5GBFileFromDownload(true); | ||
| 120 | if (logPath == null) | ||
| 121 | { | ||
| 122 | return; | ||
| 123 | } | ||
| 124 | |||
| 125 | Assert.True(LogVerifier.MessageInLogFile(logPath, "Range request not supported for URL: http://localhost:9999/e2e/BundleC/fivegb.file")); | ||
| 126 | Assert.True(LogVerifier.MessageInLogFile(logPath, "Content-Length not returned for URL: http://localhost:9999/e2e/BundleC/fivegb.file")); | ||
| 127 | } | ||
| 128 | |||
| 56 | [Fact] | 129 | [Fact] |
| 57 | public void CanDownloadPayloadsFromMissingAttachedContainer() | 130 | public void CanDownloadPayloadsFromMissingAttachedContainer() |
| 58 | { | 131 | { |
diff --git a/src/test/burn/WixToolsetTest.BurnE2E/IWebServer.cs b/src/test/burn/WixToolsetTest.BurnE2E/IWebServer.cs index 3846ae94..a4d46d48 100644 --- a/src/test/burn/WixToolsetTest.BurnE2E/IWebServer.cs +++ b/src/test/burn/WixToolsetTest.BurnE2E/IWebServer.cs | |||
| @@ -8,6 +8,7 @@ namespace WixToolsetTest.BurnE2E | |||
| 8 | public interface IWebServer : IDisposable | 8 | public interface IWebServer : IDisposable |
| 9 | { | 9 | { |
| 10 | bool DisableHeadResponses { get; set; } | 10 | bool DisableHeadResponses { get; set; } |
| 11 | bool DisableRangeRequests { get; set; } | ||
| 11 | 12 | ||
| 12 | /// <summary> | 13 | /// <summary> |
| 13 | /// Registers a collection of relative URLs (the key) with its absolute path to the file (the value). | 14 | /// Registers a collection of relative URLs (the key) with its absolute path to the file (the value). |
diff --git a/src/test/burn/WixToolsetTest.BurnE2E/WebServer/CoreOwinWebServer.cs b/src/test/burn/WixToolsetTest.BurnE2E/WebServer/CoreOwinWebServer.cs index 025e01ff..8066cea5 100644 --- a/src/test/burn/WixToolsetTest.BurnE2E/WebServer/CoreOwinWebServer.cs +++ b/src/test/burn/WixToolsetTest.BurnE2E/WebServer/CoreOwinWebServer.cs | |||
| @@ -5,8 +5,10 @@ namespace WixToolsetTest.BurnE2E | |||
| 5 | using System; | 5 | using System; |
| 6 | using System.Collections.Generic; | 6 | using System.Collections.Generic; |
| 7 | using System.IO; | 7 | using System.IO; |
| 8 | using System.Threading.Tasks; | ||
| 8 | using Microsoft.AspNetCore.Builder; | 9 | using Microsoft.AspNetCore.Builder; |
| 9 | using Microsoft.AspNetCore.Hosting; | 10 | using Microsoft.AspNetCore.Hosting; |
| 11 | using Microsoft.AspNetCore.Http; | ||
| 10 | using Microsoft.AspNetCore.StaticFiles; | 12 | using Microsoft.AspNetCore.StaticFiles; |
| 11 | using Microsoft.Extensions.FileProviders; | 13 | using Microsoft.Extensions.FileProviders; |
| 12 | using Microsoft.Extensions.FileProviders.Physical; | 14 | using Microsoft.Extensions.FileProviders.Physical; |
| @@ -15,11 +17,14 @@ namespace WixToolsetTest.BurnE2E | |||
| 15 | 17 | ||
| 16 | public class CoreOwinWebServer : IWebServer, IFileProvider | 18 | public class CoreOwinWebServer : IWebServer, IFileProvider |
| 17 | { | 19 | { |
| 20 | const string StaticFileBasePath = "/e2e"; | ||
| 21 | |||
| 18 | private Dictionary<string, string> PhysicalPathsByRelativeUrl { get; } = new Dictionary<string, string>(); | 22 | private Dictionary<string, string> PhysicalPathsByRelativeUrl { get; } = new Dictionary<string, string>(); |
| 19 | 23 | ||
| 20 | private IHost WebHost { get; set; } | 24 | private IHost WebHost { get; set; } |
| 21 | 25 | ||
| 22 | public bool DisableHeadResponses { get; set; } | 26 | public bool DisableHeadResponses { get; set; } |
| 27 | public bool DisableRangeRequests { get; set; } | ||
| 23 | 28 | ||
| 24 | public void AddFiles(Dictionary<string, string> physicalPathsByRelativeUrl) | 29 | public void AddFiles(Dictionary<string, string> physicalPathsByRelativeUrl) |
| 25 | { | 30 | { |
| @@ -38,10 +43,11 @@ namespace WixToolsetTest.BurnE2E | |||
| 38 | webBuilder.UseUrls("http://localhost:9999"); | 43 | webBuilder.UseUrls("http://localhost:9999"); |
| 39 | webBuilder.Configure(appBuilder => | 44 | webBuilder.Configure(appBuilder => |
| 40 | { | 45 | { |
| 46 | appBuilder.Use(this.CustomStaticFileMiddleware); | ||
| 41 | appBuilder.UseStaticFiles(new StaticFileOptions | 47 | appBuilder.UseStaticFiles(new StaticFileOptions |
| 42 | { | 48 | { |
| 43 | FileProvider = this, | 49 | FileProvider = this, |
| 44 | RequestPath = "/e2e", | 50 | RequestPath = StaticFileBasePath, |
| 45 | ServeUnknownFileTypes = true, | 51 | ServeUnknownFileTypes = true, |
| 46 | OnPrepareResponse = this.OnPrepareStaticFileResponse, | 52 | OnPrepareResponse = this.OnPrepareStaticFileResponse, |
| 47 | }); | 53 | }); |
| @@ -51,6 +57,42 @@ namespace WixToolsetTest.BurnE2E | |||
| 51 | this.WebHost.Start(); | 57 | this.WebHost.Start(); |
| 52 | } | 58 | } |
| 53 | 59 | ||
| 60 | private async Task CustomStaticFileMiddleware(HttpContext context, Func<Task> next) | ||
| 61 | { | ||
| 62 | if (!this.DisableRangeRequests || (!HttpMethods.IsGet(context.Request.Method) && !HttpMethods.IsHead(context.Request.Method))) | ||
| 63 | { | ||
| 64 | await next(); | ||
| 65 | return; | ||
| 66 | } | ||
| 67 | |||
| 68 | // Only send Content-Length header. | ||
| 69 | // Don't support range requests. | ||
| 70 | // https://github.com/dotnet/aspnetcore/blob/60abfafe32a4692f9dc4a172665524f163b10012/src/Middleware/StaticFiles/src/StaticFileMiddleware.cs | ||
| 71 | if (!context.Request.Path.StartsWithSegments(StaticFileBasePath, out var subpath)) | ||
| 72 | { | ||
| 73 | context.Response.StatusCode = 404; | ||
| 74 | return; | ||
| 75 | } | ||
| 76 | |||
| 77 | var fileInfo = this.GetFileInfo(subpath); | ||
| 78 | if (!fileInfo.Exists) | ||
| 79 | { | ||
| 80 | context.Response.StatusCode = 404; | ||
| 81 | return; | ||
| 82 | } | ||
| 83 | |||
| 84 | var responseHeaders = context.Response.GetTypedHeaders(); | ||
| 85 | var fileLength = fileInfo.Length; | ||
| 86 | responseHeaders.ContentLength = fileLength; | ||
| 87 | |||
| 88 | this.OnPrepareStaticFileResponse(new StaticFileResponseContext(context, fileInfo)); | ||
| 89 | |||
| 90 | if (HttpMethods.IsGet(context.Request.Method)) | ||
| 91 | { | ||
| 92 | await context.Response.SendFileAsync(fileInfo, 0, fileLength); | ||
| 93 | } | ||
| 94 | } | ||
| 95 | |||
| 54 | private void OnPrepareStaticFileResponse(StaticFileResponseContext obj) | 96 | private void OnPrepareStaticFileResponse(StaticFileResponseContext obj) |
| 55 | { | 97 | { |
| 56 | if (this.DisableHeadResponses && obj.Context.Request.Method == "HEAD") | 98 | if (this.DisableHeadResponses && obj.Context.Request.Method == "HEAD") |
