using System; using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.SpaServices.Webpack { /// /// Based on https://github.com/aspnet/Proxy/blob/dev/src/Microsoft.AspNetCore.Proxy/ProxyMiddleware.cs /// Differs in that, if the proxied request returns a 404, we pass through to the next middleware in the chain /// This is useful for Webpack middleware, because it lets you fall back on prebuilt files on disk for /// chunks not exposed by the current Webpack config (e.g., DLL/vendor chunks). /// internal class ConditionalProxyMiddleware { private const int DefaultHttpBufferSize = 4096; private readonly HttpClient _httpClient; private readonly RequestDelegate _next; private readonly ConditionalProxyMiddlewareOptions _options; private readonly string _pathPrefix; public ConditionalProxyMiddleware( RequestDelegate next, string pathPrefix, ConditionalProxyMiddlewareOptions options) { if (!pathPrefix.StartsWith("/")) { pathPrefix = "/" + pathPrefix; } _next = next; _pathPrefix = pathPrefix; _options = options; _httpClient = new HttpClient(new HttpClientHandler()); _httpClient.Timeout = _options.RequestTimeout; } public async Task Invoke(HttpContext context) { if (context.Request.Path.StartsWithSegments(_pathPrefix)) { var didProxyRequest = await PerformProxyRequest(context); if (didProxyRequest) { return; } } // Not a request we can proxy await _next.Invoke(context); } private async Task PerformProxyRequest(HttpContext context) { var requestMessage = new HttpRequestMessage(); // Copy the request headers foreach (var header in context.Request.Headers) { if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray())) { requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()); } } requestMessage.Headers.Host = _options.Host + ":" + _options.Port; var uriString = $"{_options.Scheme}://{_options.Host}:{_options.Port}{context.Request.Path}{context.Request.QueryString}"; requestMessage.RequestUri = new Uri(uriString); requestMessage.Method = new HttpMethod(context.Request.Method); using ( var responseMessage = await _httpClient.SendAsync( requestMessage, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted)) { if (responseMessage.StatusCode == HttpStatusCode.NotFound) { // Let some other middleware handle this return false; } // We can handle this context.Response.StatusCode = (int) responseMessage.StatusCode; foreach (var header in responseMessage.Headers) { context.Response.Headers[header.Key] = header.Value.ToArray(); } foreach (var header in responseMessage.Content.Headers) { context.Response.Headers[header.Key] = header.Value.ToArray(); } // SendAsync removes chunking from the response. This removes the header so it doesn't expect a chunked response. context.Response.Headers.Remove("transfer-encoding"); using (var responseStream = await responseMessage.Content.ReadAsStreamAsync()) { try { await responseStream.CopyToAsync(context.Response.Body, DefaultHttpBufferSize, context.RequestAborted); } catch (OperationCanceledException) { // The CopyToAsync task will be canceled if the client disconnects (e.g., user // closes or refreshes the browser tab). Don't treat this as an error. } } return true; } } } }