using System.Net; using System.Net.Http.Headers; using Microsoft.Extensions.Options; namespace Eis.Web.Legacy; public sealed class LegacyApiProxy( HttpClient httpClient, IOptions options, ILogger logger) { private static readonly HashSet HopByHopHeaders = new(StringComparer.OrdinalIgnoreCase) { "Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "TE", "Trailer", "Transfer-Encoding", "Upgrade" }; private readonly LegacyNodeOptions _options = options.Value; public async Task ForwardAsync(HttpContext context) { if (!_options.Enabled) { context.Response.StatusCode = StatusCodes.Status501NotImplemented; await context.Response.WriteAsJsonAsync(new { ok = false, message = "该接口尚未迁移到 ASP.NET Core", migration = new { native = false, legacyProxyEnabled = false } }, context.RequestAborted); return; } var target = new Uri(_options.BaseUrl, $"{context.Request.PathBase}{context.Request.Path}{context.Request.QueryString}"); using var outbound = CreateRequest(context, target); try { using var upstream = await httpClient.SendAsync( outbound, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted); context.Response.StatusCode = (int)upstream.StatusCode; CopyResponseHeaders(upstream, context.Response); if (context.Request.Method != HttpMethods.Head && upstream.StatusCode != HttpStatusCode.NoContent) { await upstream.Content.CopyToAsync(context.Response.Body, context.RequestAborted); } } catch (HttpRequestException exception) { logger.LogWarning(exception, "Legacy Node API at {Target} is unavailable", target); if (context.Response.HasStarted) { context.Abort(); return; } context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; await context.Response.WriteAsJsonAsync(new { ok = false, message = "迁移期间的旧版 API 服务暂时不可用", migration = new { native = false, legacyProxyEnabled = true } }, context.RequestAborted); } } public async Task IsAvailableAsync(CancellationToken cancellationToken) { if (!_options.Enabled) { return true; } try { using var response = await httpClient.GetAsync("api/public/home", cancellationToken); return response.IsSuccessStatusCode; } catch (HttpRequestException) { return false; } } private static HttpRequestMessage CreateRequest(HttpContext context, Uri target) { var request = new HttpRequestMessage(new HttpMethod(context.Request.Method), target); var hasBody = context.Request.ContentLength > 0 || context.Request.Headers.TransferEncoding.Count > 0; if (hasBody) { request.Content = new StreamContent(context.Request.Body); } foreach (var (name, values) in context.Request.Headers) { if (name.Equals("Host", StringComparison.OrdinalIgnoreCase) || HopByHopHeaders.Contains(name)) { continue; } var valueArray = values.ToArray(); if (!request.Headers.TryAddWithoutValidation(name, valueArray) && request.Content is not null) { request.Content.Headers.TryAddWithoutValidation(name, valueArray); } } request.Headers.TryAddWithoutValidation("X-Forwarded-Host", context.Request.Host.Value); request.Headers.TryAddWithoutValidation("X-Forwarded-Proto", context.Request.Scheme); return request; } private static void CopyResponseHeaders(HttpResponseMessage upstream, HttpResponse response) { foreach (var header in upstream.Headers.Concat(upstream.Content.Headers)) { if (!HopByHopHeaders.Contains(header.Key)) { response.Headers.Append(header.Key, header.Value.ToArray()); } } response.Headers.Remove("transfer-encoding"); } }