Files
CapySR/src/CapySR.SdkServer/ResourceMirror.cs
T

91 lines
3.2 KiB
C#
Raw Normal View History

2026-09-07 21:28:11 -07:00
using System.Net;
using CapySR.Common;
namespace CapySR.SdkServer;
// Clients sometimes resolve autopatchcn.bhsr.com to a China-mainland edge that is
// unreachable from elsewhere, so the design-data download times out and the game hangs
// on the loading bar. We can reach the CDN, so proxy it and hand the client our own url.
public sealed class ResourceMirror(HotfixCache cache, ILogger<ResourceMirror> logger)
{
public const string Route = "/mirror/{version}/{kind}/{**path}";
private static readonly HttpClient Upstream = new(new HttpClientHandler
{
UseProxy = false,
AutomaticDecompression = DecompressionMethods.All,
})
{
Timeout = TimeSpan.FromMinutes(10),
};
public static string UrlFor(DispatchConfig dispatch, string version, string kind) =>
$"http://{dispatch.PublicHost}:{dispatch.Port}/mirror/{Uri.EscapeDataString(version)}/{kind}";
public async Task HandleAsync(HttpContext context, string version, string kind, string? path)
{
var hotfix = await cache.GetAsync(version, string.Empty);
var baseUrl = hotfix is null ? null : kind switch
{
"asb" => hotfix.AssetBundleUrl,
"ex" => hotfix.ExResourceUrl,
"lua" => hotfix.LuaUrl,
"ifix" => hotfix.IfixUrl,
_ => null,
};
if (string.IsNullOrEmpty(baseUrl))
{
logger.LogWarning("mirror: no {Kind} url for {Version}", kind, version);
context.Response.StatusCode = StatusCodes.Status404NotFound;
return;
}
var target = $"{baseUrl.TrimEnd('/')}/{path}";
using var request = new HttpRequestMessage(HttpMethod.Get, target);
// the client resumes partial downloads, so range headers have to survive the hop
if (context.Request.Headers.Range.Count > 0)
{
request.Headers.TryAddWithoutValidation("Range", (string?)context.Request.Headers.Range!);
}
try
{
using var response = await Upstream.SendAsync(
request, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted);
context.Response.StatusCode = (int)response.StatusCode;
foreach (var header in response.Content.Headers)
{
if (header.Key is "Content-Length" or "Content-Type" or "Content-Range" or "Last-Modified")
{
context.Response.Headers[header.Key] = header.Value.ToArray();
}
}
if (response.Headers.AcceptRanges.Count > 0)
{
context.Response.Headers.AcceptRanges = "bytes";
}
await response.Content.CopyToAsync(context.Response.Body, context.RequestAborted);
}
catch (OperationCanceledException)
{
// client gave up on the download; nothing to report
}
catch (HttpRequestException e)
{
logger.LogError("mirror: {Target} failed: {Message}", target, e.Message);
if (!context.Response.HasStarted)
{
context.Response.StatusCode = StatusCodes.Status502BadGateway;
}
}
}
}