fix(launcher): harden updater crash recovery

This commit is contained in:
Erik 2026-08-14 23:12:15 +02:00
parent 2d2a5b5046
commit 1955ca8ab5
27 changed files with 2714 additions and 544 deletions

View file

@ -1,4 +1,5 @@
using System.Buffers;
using System.Net;
using System.Security.Cryptography;
namespace AcDream.Launcher.Core.Updates;
@ -55,15 +56,11 @@ public sealed class VerifiedArtifactDownloader
bool ownsDestination = false;
try
{
using HttpResponseMessage response = await _httpClient.GetAsync(
using HttpResponseMessage response = await SendWithValidatedRedirectsAsync(
artifact.Url,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
ReleaseManifestClient.RequireSecureOrLoopback(
response.RequestMessage?.RequestUri ?? artifact.Url,
"artifact redirect");
if (response.Content.Headers.ContentLength is long contentLength
&& contentLength != artifact.Size)
{
@ -183,6 +180,86 @@ public sealed class VerifiedArtifactDownloader
}
}
private async Task<HttpResponseMessage> SendWithValidatedRedirectsAsync(
Uri initialUri,
CancellationToken cancellationToken)
{
bool allowLoopbackHttp = initialUri.Scheme == Uri.UriSchemeHttp
&& initialUri.IsLoopback;
Uri current = initialUri;
var visited = new HashSet<string>(StringComparer.Ordinal);
for (int redirectCount = 0;;)
{
ReleaseManifestClient.RequireTransport(
current,
"artifact redirect",
allowLoopbackHttp);
if (!visited.Add(current.AbsoluteUri))
{
throw new LauncherUpdateException(
"The release artifact redirect chain contains a loop.");
}
using var request = new HttpRequestMessage(HttpMethod.Get, current);
HttpResponseMessage response = await _httpClient.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken)
.ConfigureAwait(false);
Uri effectiveUri = response.RequestMessage?.RequestUri ?? current;
if (!Uri.Equals(effectiveUri, current))
{
response.Dispose();
throw new LauncherUpdateException(
"The artifact HTTP transport followed an automatic redirect; "
+ "every redirect must be validated before it is requested.");
}
if (!IsRedirect(response.StatusCode))
{
return response;
}
try
{
if (redirectCount >= ReleaseManifestClient.MaximumRedirects)
{
throw new LauncherUpdateException(
$"The release artifact exceeded "
+ $"{ReleaseManifestClient.MaximumRedirects} redirects.");
}
Uri? location = response.Headers.Location;
if (location is null)
{
throw new LauncherUpdateException(
"The release artifact redirect has no Location header.");
}
Uri next = location.IsAbsoluteUri
? location
: new Uri(current, location);
ReleaseManifestClient.RequireTransport(
next,
"artifact redirect",
allowLoopbackHttp);
current = next;
redirectCount++;
}
finally
{
response.Dispose();
}
}
}
private static bool IsRedirect(HttpStatusCode statusCode) => statusCode is
HttpStatusCode.MovedPermanently
or HttpStatusCode.Found
or HttpStatusCode.SeeOther
or HttpStatusCode.TemporaryRedirect
or HttpStatusCode.PermanentRedirect;
internal static void TryDelete(string path)
{
try