@joebigelow / wix-1 / commits / 28c0a27d

Handle missing content length with range request and empty files. Add test for server without range request support.

Handle missing content length with range request and empty files. Add test for server without range request support.

Sean Hall committed Feb 18, 2022 at 17:56 UTC 28c0a27ddf03dcf07a11c291699428a32f381fbc
5 files changed +161 -13
src/libs/dutil/WixToolset.DUtil/dlutil.cpp
+39 -7
@@ -295,7 +295,10 @@ static HRESULT DownloadResource(
295 HANDLE hPayloadFile = INVALID_HANDLE_VALUE;
296 DWORD cbMaxData = 64 * 1024; // 64 KB
297 BYTE* pbData = NULL;
298 - BOOL fRangeRequestsAccepted = TRUE;
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;
@@ -315,10 +318,19 @@ static HRESULT DownloadResource(
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.
318 - while (fRangeRequestsAccepted && (0 == dw64ResourceLength || dw64ResumeOffset < dw64ResourceLength))
321 + for (;;)
322 {
320 - hr = AllocateRangeRequestHeader(dw64ResumeOffset, 0 == dw64ResourceLength ? dw64AuthoredResourceLength : dw64ResourceLength, &sczRangeRequestHeader);
321 - DlExitOnFailure(hr, "Failed to allocate range request header.");
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);
@@ -326,6 +338,13 @@ static HRESULT DownloadResource(
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)
@@ -337,23 +356,36 @@ static HRESULT DownloadResource(
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
348 - // If we just tried to do a range request and found out that it isn't supported, start over.
349 - if (!fRangeRequestsAccepted)
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 {
351 - // TODO: log a message that the server did not accept range requests.
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:
src/test/burn/TestData/CacheTests/BundleC/BundleC.wxs
+1 -1
@@ -5,7 +5,7 @@
5 <Fragment>
6 <PackageGroup Id="BundlePackages">
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>
10 </PackageGroup>
11 </Fragment>
src/test/burn/WixToolsetTest.BurnE2E/CacheTests.cs
+77 -4
@@ -15,8 +15,7 @@ namespace WixToolsetTest.BurnE2E
15 {
16 public CacheTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { }
17
18 - [Fact]
19 - public void CanCache5GBFile()
18 + private bool Is5GBFileAvailable()
19 {
20 // Recreate the 5GB payload to avoid having to copy it to the VM to run the tests.
21 const long FiveGB = 5_368_709_120;
@@ -28,8 +27,8 @@ namespace WixToolsetTest.BurnE2E
27 var drive = new DriveInfo(targetFilePath.Substring(0, 1));
28 if (drive.AvailableFreeSpace < FiveGB + OneGB)
29 {
31 - Console.WriteLine("Skipping CanCache5GBFile() test because there is not enough disk space available to run the test.");
32 - return;
30 + Console.WriteLine($"Skipping {this.TestContext.TestName} because there is not enough disk space available to run the test.");
31 + return false;
32 }
33
34 if (!File.Exists(targetFilePath))
@@ -42,6 +41,17 @@ namespace WixToolsetTest.BurnE2E
41 testTool.Run(true);
42 }
43
44 + return true;
45 + }
46 +
47 + [Fact]
48 + public void CanCache5GBFile()
49 + {
50 + if (!this.Is5GBFileAvailable())
51 + {
52 + return;
53 + }
54 +
55 var packageA = this.CreatePackageInstaller("PackageA");
56 var bundleC = this.CreateBundleInstaller("BundleC");
57
@@ -53,6 +63,69 @@ namespace WixToolsetTest.BurnE2E
63 packageA.VerifyInstalled(true);
64 }
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 +
129 [Fact]
130 public void CanDownloadPayloadsFromMissingAttachedContainer()
131 {
src/test/burn/WixToolsetTest.BurnE2E/IWebServer.cs
+1
@@ -8,6 +8,7 @@ namespace WixToolsetTest.BurnE2E
8 public interface IWebServer : IDisposable
9 {
10 bool DisableHeadResponses { get; set; }
11 + bool DisableRangeRequests { get; set; }
12
13 /// <summary>
14 /// Registers a collection of relative URLs (the key) with its absolute path to the file (the value).
src/test/burn/WixToolsetTest.BurnE2E/WebServer/CoreOwinWebServer.cs
+43 -1
@@ -5,8 +5,10 @@ namespace WixToolsetTest.BurnE2E
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8 + using System.Threading.Tasks;
9 using Microsoft.AspNetCore.Builder;
10 using Microsoft.AspNetCore.Hosting;
11 + using Microsoft.AspNetCore.Http;
12 using Microsoft.AspNetCore.StaticFiles;
13 using Microsoft.Extensions.FileProviders;
14 using Microsoft.Extensions.FileProviders.Physical;
@@ -15,11 +17,14 @@ namespace WixToolsetTest.BurnE2E
17
18 public class CoreOwinWebServer : IWebServer, IFileProvider
19 {
20 + const string StaticFileBasePath = "/e2e";
21 +
22 private Dictionary<string, string> PhysicalPathsByRelativeUrl { get; } = new Dictionary<string, string>();
23
24 private IHost WebHost { get; set; }
25
26 public bool DisableHeadResponses { get; set; }
27 + public bool DisableRangeRequests { get; set; }
28
29 public void AddFiles(Dictionary<string, string> physicalPathsByRelativeUrl)
30 {
@@ -38,10 +43,11 @@ namespace WixToolsetTest.BurnE2E
43 webBuilder.UseUrls("http://localhost:9999");
44 webBuilder.Configure(appBuilder =>
45 {
46 + appBuilder.Use(this.CustomStaticFileMiddleware);
47 appBuilder.UseStaticFiles(new StaticFileOptions
48 {
49 FileProvider = this,
44 - RequestPath = "/e2e",
50 + RequestPath = StaticFileBasePath,
51 ServeUnknownFileTypes = true,
52 OnPrepareResponse = this.OnPrepareStaticFileResponse,
53 });
@@ -51,6 +57,42 @@ namespace WixToolsetTest.BurnE2E
57 this.WebHost.Start();
58 }
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 +
96 private void OnPrepareStaticFileResponse(StaticFileResponseContext obj)
97 {
98 if (this.DisableHeadResponses && obj.Context.Request.Method == "HEAD")