public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix) { if (ShouldCopyRequestHeaders.GetValueOrDefault(true)) { await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix); } if (RequestTransforms.Count == 0) { return; } var transformContext = new RequestTransformContext() { DestinationPrefix = destinationPrefix, HttpContext = httpContext, ProxyRequest = proxyRequest, Path = httpContext.Request.Path, Query = new QueryTransformContext(httpContext.Request), HeadersCopied = ShouldCopyRequestHeaders.GetValueOrDefault(true), }; foreach (var requestTransform in RequestTransforms) { await requestTransform.ApplyAsync(transformContext); } // Allow a transform to directly set a custom RequestUri. proxyRequest.RequestUri ??= RequestUtilities.MakeDestinationAddress( transformContext.DestinationPrefix, transformContext.Path, transformContext.Query.QueryString); }
private async ValueTask <(HttpRequestMessage, StreamCopyHttpContent?)> CreateRequestMessageAsync(HttpContext context, string destinationPrefix, HttpTransformer transformer, ForwarderRequestConfig?requestConfig, bool isStreamingRequest, ActivityCancellationTokenSource activityToken) { // "http://a".Length = 8 if (destinationPrefix == null || destinationPrefix.Length < 8) { throw new ArgumentException("Invalid destination prefix.", nameof(destinationPrefix)); } var destinationRequest = new HttpRequestMessage(); destinationRequest.Method = RequestUtilities.GetHttpMethod(context.Request.Method); var upgradeFeature = context.Features.Get <IHttpUpgradeFeature>(); var upgradeHeader = context.Request.Headers[HeaderNames.Upgrade].ToString(); var isUpgradeRequest = (upgradeFeature?.IsUpgradableRequest ?? false) // Mitigate https://github.com/microsoft/reverse-proxy/issues/255, IIS considers all requests upgradeable. && (string.Equals("WebSocket", upgradeHeader, StringComparison.OrdinalIgnoreCase) // https://github.com/microsoft/reverse-proxy/issues/467 for kubernetes APIs || upgradeHeader.StartsWith("SPDY/", StringComparison.OrdinalIgnoreCase)); // Default to HTTP/1.1 for proxying upgradeable requests. This is already the default as of .NET Core 3.1 // Otherwise request what's set in proxyOptions (e.g. default HTTP/2) and let HttpClient negotiate the protocol // based on VersionPolicy (for .NET 5 and higher). For example, downgrading to HTTP/1.1 if it cannot establish HTTP/2 with the target. // This is done without extra round-trips thanks to ALPN. We can detect a downgrade after calling HttpClient.SendAsync // (see Step 3 below). TBD how this will change when HTTP/3 is supported. destinationRequest.Version = isUpgradeRequest ? ProtocolHelper.Http11Version : (requestConfig?.Version ?? DefaultVersion); #if NET destinationRequest.VersionPolicy = isUpgradeRequest ? HttpVersionPolicy.RequestVersionOrLower : (requestConfig?.VersionPolicy ?? DefaultVersionPolicy); #endif // :: Step 2: Setup copy of request body (background) Client --► Proxy --► Destination // Note that we must do this before step (3) because step (3) may also add headers to the HttpContent that we set up here. var requestContent = SetupRequestBodyCopy(context.Request, isStreamingRequest, activityToken); destinationRequest.Content = requestContent; // :: Step 3: Copy request headers Client --► Proxy --► Destination await transformer.TransformRequestAsync(context, destinationRequest, destinationPrefix); if (isUpgradeRequest) { RestoreUpgradeHeaders(context, destinationRequest); } // Allow someone to custom build the request uri, otherwise provide a default for them. var request = context.Request; destinationRequest.RequestUri ??= RequestUtilities.MakeDestinationAddress(destinationPrefix, request.Path, request.QueryString); Log.Proxying(_logger, destinationRequest, isStreamingRequest); if (requestConfig?.AllowResponseBuffering != true) { context.Features.Get <IHttpResponseBodyFeature>()?.DisableBuffering(); } // TODO: What if they replace the HttpContent object? That would mess with our tracking and error handling. return(destinationRequest, requestContent); }
/// <summary> /// A callback that is invoked prior to sending the proxied request. All HttpRequestMessage /// fields are initialized except RequestUri, which will be initialized after the /// callback if no value is provided. The string parameter represents the destination /// URI prefix that should be used when constructing the RequestUri. The headers /// are copied by the base implementation, excluding some protocol headers like HTTP/2 /// pseudo headers (":authority"). /// </summary> /// <param name="httpContext">The incoming request.</param> /// <param name="proxyRequest">The outgoing proxy request.</param> /// <param name="destinationPrefix">The uri prefix for the selected destination server which can be used to create /// the RequestUri.</param> public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix) { // Copy all request headers await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix); // Customize the query string: var queryContext = new QueryTransformContext(httpContext.Request); queryContext.Collection.Remove("param1"); queryContext.Collection["area"] = "xx2"; // Assign the custom uri. Be careful about extra slashes when concatenating here. RequestUtilities.MakeDestinationAddress is a safe default. proxyRequest.RequestUri = RequestUtilities.MakeDestinationAddress("https://example.com", httpContext.Request.Path, queryContext.QueryString); // Suppress the original request header, use the one from the destination Uri. proxyRequest.Headers.Host = null; }
// These intentionally do not call base because the logic here overlaps with the default header copy logic. public override Task TransformRequestAsync(HttpContext context, HttpRequestMessage proxyRequest, string destinationPrefix) { var transformContext = new RequestParametersTransformContext() { DestinationPrefix = destinationPrefix, HttpContext = context, ProxyRequest = proxyRequest, Path = context.Request.Path, Query = new QueryTransformContext(context.Request), }; foreach (var requestTransform in RequestTransforms) { requestTransform.Apply(transformContext); } // Allow a transform to directly set a custom RequestUri. proxyRequest.RequestUri ??= RequestUtilities.MakeDestinationAddress( transformContext.DestinationPrefix, transformContext.Path, transformContext.Query.QueryString); CopyRequestHeaders(context, proxyRequest); return(Task.CompletedTask); }
public void MakeDestinationAddress(string destinationPrefix, string path, string query, string expected) { var uri = RequestUtilities.MakeDestinationAddress(destinationPrefix, new PathString(path), new QueryString(query)); Assert.Equal(expected, uri.AbsoluteUri); }
/// <summary> /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. /// </summary> public void Configure(IApplicationBuilder app, IHttpForwarder forwarder) { // Configure our own HttpMessageInvoker for outbound calls for proxy operations var httpClient = new HttpMessageInvoker(new SocketsHttpHandler() { UseProxy = false, AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None, UseCookies = false }); // Setup our own request transform class var transformer = new CustomTransformer(); // or HttpTransformer.Default; var requestOptions = new ForwarderRequestConfig { ActivityTimeout = TimeSpan.FromSeconds(100) }; app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.Map("/test/{**catch-all}", async httpContext => { var error = await forwarder.SendAsync(httpContext, "https://example.com", httpClient, requestOptions, static (context, proxyRequest) => { // Customize the query string: var queryContext = new QueryTransformContext(context.Request); queryContext.Collection.Remove("param1"); queryContext.Collection["area"] = "xx2"; // Assign the custom uri. Be careful about extra slashes when concatenating here. RequestUtilities.MakeDestinationAddress is a safe default. proxyRequest.RequestUri = RequestUtilities.MakeDestinationAddress("https://example.com", context.Request.Path, queryContext.QueryString); // Suppress the original request header, use the one from the destination Uri. proxyRequest.Headers.Host = null; return(default); }); // Check if the proxy operation was successful if (error != ForwarderError.None) { var errorFeature = httpContext.Features.Get <IForwarderErrorFeature>(); var exception = errorFeature.Exception; } }); // When using IHttpForwarder for direct forwarding you are responsible for routing, destination discovery, load balancing, affinity, etc.. // For an alternate example that includes those features see BasicYarpSample. endpoints.Map("/{**catch-all}", async httpContext => { var error = await forwarder.SendAsync(httpContext, "https://example.com", httpClient, requestOptions, transformer); // Check if the proxy operation was successful if (error != ForwarderError.None) { var errorFeature = httpContext.Features.Get <IForwarderErrorFeature>(); var exception = errorFeature.Exception; } }); });