/// <summary> /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. /// </summary> public void Configure(IApplicationBuilder app, IHttpProxy httpProxy) { var httpClient = new HttpMessageInvoker(new SocketsHttpHandler() { UseProxy = false, AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None, UseCookies = false }); // Copy all request headers except Host var transformer = new CustomTransformer(); // or HttpTransformer.Default; var requestOptions = new RequestProxyOptions(TimeSpan.FromSeconds(100), null); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); endpoints.Map("/{**catch-all}", async httpContext => { await httpProxy.ProxyAsync(httpContext, "https://localhost:10000/", httpClient, requestOptions, transformer); var errorFeature = httpContext.Features.Get <IProxyErrorFeature>(); if (errorFeature != null) { var error = errorFeature.Error; var exception = errorFeature.Exception; } }); }); }
public async Task <RequestProxyOptions> CreateProxyOptionsAsync() { var credentialsProvider = httpContext.RequestServices.GetRequiredService <GraphCredentialsProvider>(); var credentials = credentialsProvider.GetCredentials(); var token = await credentials.GetTokenAsync(new TokenRequestContext(new[] { "https://graph.microsoft.com/.default" }), CancellationToken.None); var proxyOptions = new RequestProxyOptions() { RequestTimeout = TimeSpan.FromSeconds(100), // Copy all request headers except Host Transforms = new Transforms( copyRequestHeaders: true, requestTransforms: Array.Empty <RequestParametersTransform>(), requestHeaderTransforms: new Dictionary <string, RequestHeaderTransform>() { { HeaderNames.Host, new RequestHeaderValueTransform(string.Empty, append: false) }, { HeaderNames.Authorization, new RequestHeaderValueTransform($"Bearer {token.Token}", append: false) } }, responseHeaderTransforms: new Dictionary <string, ResponseHeaderTransform>(), responseTrailerTransforms: new Dictionary <string, ResponseHeaderTransform>()) }; return(proxyOptions); }
/// <inheritdoc/> public async Task Invoke(HttpContext context) { _ = context ?? throw new ArgumentNullException(nameof(context)); var reverseProxyFeature = context.GetRequiredProxyFeature(); var destinations = reverseProxyFeature.AvailableDestinations ?? throw new InvalidOperationException($"The {nameof(IReverseProxyFeature)} Destinations collection was not set."); var routeConfig = context.GetRequiredRouteConfig(); var cluster = routeConfig.Cluster; if (destinations.Count == 0) { Log.NoAvailableDestinations(_logger, cluster.ClusterId); context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; context.Features.Set <IProxyErrorFeature>(new ProxyErrorFeature(ProxyError.NoAvailableDestinations, ex: null)); return; } var destination = destinations[0]; if (destinations.Count > 1) { var random = _randomFactory.CreateRandomInstance(); Log.MultipleDestinationsAvailable(_logger, cluster.ClusterId); destination = destinations[random.Next(destinations.Count)]; } reverseProxyFeature.SelectedDestination = destination; var destinationConfig = destination.Config; if (destinationConfig == null) { throw new InvalidOperationException($"Chosen destination has no configs set: '{destination.DestinationId}'"); } // TODO: Make this configurable on a route rather than create it per request? var proxyOptions = new RequestProxyOptions() { Transforms = routeConfig.Transforms, }; try { cluster.ConcurrencyCounter.Increment(); destination.ConcurrencyCounter.Increment(); await _operationLogger.ExecuteAsync( "ReverseProxy.Proxy", () => _httpProxy.ProxyAsync(context, destinationConfig.Address, reverseProxyFeature.ClusterConfig.HttpClient, proxyOptions)); } finally { destination.ConcurrencyCounter.Decrement(); cluster.ConcurrencyCounter.Decrement(); } }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHttpProxy httpProxy, WPMessageInvokerFactory messageInvoker, WPProxySettings settings) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseStaticFiles(); app.UseRouting(); app.UseCookiePolicy(); app.UseAuthentication(); app.UseAuthorization(); app.Use(next => context => { // Force https scheme (since request inside a docker container may appear to be http) context.Request.Scheme = "https"; return(next(context)); }); var transformer = new WPRequestTransformer(); // or HttpTransformer.Default; var requestOptions = new RequestProxyOptions { Timeout = TimeSpan.FromSeconds(100), Version = new Version(1, 1), // Not all servers support HTTP 2 VersionPolicy = HttpVersionPolicy.RequestVersionOrHigher }; app.UseEndpoints(endpoints => { // Dont't authenticate manifest.json endpoints.Map("/manifest.json", async httpContext => // { var siteSettings = settings.GetForHost(httpContext.Request.Host.ToString()); await httpProxy.ProxyAsync(httpContext, siteSettings.SourceAddress, messageInvoker.Create(), requestOptions, transformer); }); endpoints.Map("/{**catch-all}", async httpContext => // { var siteSettings = settings.GetForHost(httpContext.Request.Host.ToString()); await httpProxy.ProxyAsync(httpContext, siteSettings.SourceAddress, messageInvoker.Create(), requestOptions, transformer); var errorFeature = httpContext.Features.Get <IProxyErrorFeature>(); if (errorFeature != null) { var error = errorFeature.Error; var exception = errorFeature.Exception; throw errorFeature.Exception; } }) .RequireAuthorization(WPProxySettings.AuthorizationPolicy); endpoints.MapDefaultControllerRoute(); }); }
public static IEndpointRouteBuilder MapFacadeProxy(this IEndpointRouteBuilder endpoints) { var httpClient = new HttpMessageInvoker(new SocketsHttpHandler() { UseProxy = false, AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None, UseCookies = false }); var transformer = new RedirectTransformer(); var requestOptions = new RequestProxyOptions(TimeSpan.FromSeconds(100), null); var httpProxy = endpoints.ServiceProvider.GetRequiredService <IHttpProxy>(); endpoints.Map("/{**catch-all}", async httpContext => { await httpProxy.ProxyAsync(httpContext, "http://localhost:24019/", httpClient, requestOptions, transformer); }); return(endpoints); }
public ClusterConfig( Cluster cluster, ClusterHealthCheckOptions healthCheckOptions, ClusterLoadBalancingOptions loadBalancingOptions, ClusterSessionAffinityOptions sessionAffinityOptions, HttpMessageInvoker httpClient, ClusterProxyHttpClientOptions httpClientOptions, RequestProxyOptions httpRequestOptions, IReadOnlyDictionary<string, string> metadata) { _cluster = cluster; HealthCheckOptions = healthCheckOptions; LoadBalancingOptions = loadBalancingOptions; SessionAffinityOptions = sessionAffinityOptions; HttpClient = httpClient; HttpClientOptions = httpClientOptions; HttpRequestOptions = httpRequestOptions; Metadata = metadata; }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHttpProxy httpProxy) { var httpClient = new HttpMessageInvoker(new SocketsHttpHandler() { UseProxy = false, AllowAutoRedirect = false, AutomaticDecompression = System.Net.DecompressionMethods.None, UseCookies = false }); var transformer = new CustomTransformer(); var requestOptions = new RequestProxyOptions(); app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.Map("/{**catch-all}", async httpContext => { var urls = getUrls(); var teste = httpContext.Request.Headers.FirstOrDefault(c => c.Key == "teste").Value; var url = urls.FirstOrDefault(x => x.key == "1").url; if (teste.Count != 0) { url = urls?.FirstOrDefault(x => x.key == teste.ToString()).url; } await httpProxy.ProxyAsync(httpContext, url, httpClient, requestOptions); var errorFeature = httpContext.GetProxyErrorFeature(); if (errorFeature != null) { var error = errorFeature.Error; var exception = errorFeature.Exception; } }); }); }
/// <summary> /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. /// </summary> public void Configure(IApplicationBuilder app, IHttpProxy httpProxy) { var httpClient = new HttpMessageInvoker(new SocketsHttpHandler() { UseProxy = false, AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None, UseCookies = false }); var proxyOptions = new RequestProxyOptions() { RequestTimeout = TimeSpan.FromSeconds(100), // Copy all request headers except Host Transforms = new Transforms( copyRequestHeaders: true, requestTransforms: Array.Empty <RequestParametersTransform>(), requestHeaderTransforms: new Dictionary <string, RequestHeaderTransform>() { { HeaderNames.Host, new RequestHeaderValueTransform(string.Empty, append: false) } }, responseHeaderTransforms: new Dictionary <string, ResponseHeaderTransform>(), responseTrailerTransforms: new Dictionary <string, ResponseHeaderTransform>()) }; app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); endpoints.Map("/{**catch-all}", async httpContext => { await httpProxy.ProxyAsync(httpContext, "https://localhost:10000/", httpClient, proxyOptions); var errorFeature = httpContext.Features.Get <IProxyErrorFeature>(); if (errorFeature != null) { var error = errorFeature.Error; var exception = errorFeature.Exception; } }); }); }
/// <summary> /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. /// </summary> public void Configure(IApplicationBuilder app, IHttpProxy httpProxy) { var httpClient = new HttpMessageInvoker(new SocketsHttpHandler() { UseProxy = false, AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None, UseCookies = false }); var transformBuilder = app.ApplicationServices.GetRequiredService <ITransformBuilder>(); var transformer = transformBuilder.Create(context => { context.AddQueryRemoveKey("param1"); context.AddQueryValue("area", "xx2", false); context.AddOriginalHost(false); }); // or var transformer = new CustomTransformer(); // or var transformer = HttpTransformer.Default; var requestOptions = new RequestProxyOptions { Timeout = TimeSpan.FromSeconds(100) }; app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.Map("/{**catch-all}", async httpContext => { await httpProxy.ProxyAsync(httpContext, "https://example.com", httpClient, requestOptions, transformer); var errorFeature = httpContext.GetProxyErrorFeature(); if (errorFeature != null) { var error = errorFeature.Error; var exception = errorFeature.Exception; } }); }); }
private static RequestProxyOptions CreateProxyOptions(IConfiguration configuration) { var emptyHeaderTransforms = new Dictionary <string, ResponseHeaderTransform>(); var proxyOptions = new RequestProxyOptions() { RequestTimeout = TimeSpan.FromSeconds(100), Transforms = new RequestTransforms( copyRequestHeaders: true, requestTransforms: CreateParametersTransforms(), requestHeaderTransforms: CreateRequestHeaderTransforms(), responseHeaderTransforms: emptyHeaderTransforms, responseTrailerTransforms: emptyHeaderTransforms) }; var section = configuration.GetSection(nameof(RequestProxyOptions)); if (section.Exists()) { section.Bind(proxyOptions); } return(proxyOptions); }
public async Task Invoke_Works() { var events = TestEventListener.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Scheme = "https"; httpContext.Request.Host = new HostString("example.com"); httpContext.Request.Path = "/api/test"; httpContext.Request.QueryString = new QueryString("?a=b&c=d"); var httpClient = new HttpMessageInvoker(new Mock <HttpMessageHandler>().Object); var httpRequestOptions = new RequestProxyOptions( TimeSpan.FromSeconds(60), HttpVersion.Version11 #if NET , HttpVersionPolicy.RequestVersionExact #endif ); var cluster1 = new ClusterInfo( clusterId: "cluster1", destinationManager: new DestinationManager()); var clusterConfig = new ClusterConfig(default, default, default, default, httpClient, default, httpRequestOptions, new Dictionary <string, string>());
/// <summary> /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. /// </summary> public void Configure(IApplicationBuilder app, IHttpProxy httpProxy) { // 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 RequestProxyOptions { Timeout = TimeSpan.FromSeconds(100) }; app.UseRouting(); app.UseEndpoints(endpoints => { // When using IHttpProxy for direct proxying 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 => { await httpProxy.ProxyAsync(httpContext, "https://example.com", httpClient, requestOptions, transformer); var errorFeature = httpContext.Features.Get <IProxyErrorFeature>(); // Check if the proxy operation was successful if (errorFeature != null) { var error = errorFeature.Error; var exception = errorFeature.Exception; } }); }); }
public async Task Invoke_Works() { var events = TestEventListener.Collect(); var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Scheme = "https"; httpContext.Request.Host = new HostString("example.com"); httpContext.Request.Path = "/api/test"; httpContext.Request.QueryString = new QueryString("?a=b&c=d"); var httpClient = new HttpMessageInvoker(new Mock <HttpMessageHandler>().Object); var httpRequestOptions = new RequestProxyOptions { Timeout = TimeSpan.FromSeconds(60), Version = HttpVersion.Version11, #if NET VersionPolicy = HttpVersionPolicy.RequestVersionExact, #endif }; var cluster1 = new ClusterInfo(clusterId: "cluster1"); var clusterConfig = new ClusterConfig(new Cluster() { HttpRequest = httpRequestOptions }, httpClient); var destination1 = cluster1.Destinations.GetOrAdd( "destination1", id => new DestinationInfo(id) { Config = new DestinationConfig(new Destination { Address = "https://localhost:123/a/b/" }) }); var routeConfig = new RouteConfig( proxyRoute: new ProxyRoute() { RouteId = "Route-1" }, cluster: cluster1, transformer: null); httpContext.Features.Set <IReverseProxyFeature>( new ReverseProxyFeature() { AvailableDestinations = new List <DestinationInfo>() { destination1 }.AsReadOnly(), ClusterSnapshot = clusterConfig, RouteSnapshot = routeConfig, }); httpContext.Features.Set(cluster1); var tcs1 = new TaskCompletionSource <bool>(); var tcs2 = new TaskCompletionSource <bool>(); Mock <IHttpProxy>() .Setup(h => h.ProxyAsync( httpContext, It.Is <string>(uri => uri == "https://localhost:123/a/b/"), httpClient, It.Is <RequestProxyOptions>(requestOptions => requestOptions.Timeout == httpRequestOptions.Timeout && requestOptions.Version == httpRequestOptions.Version #if NET && requestOptions.VersionPolicy == httpRequestOptions.VersionPolicy #endif ), It.Is <HttpTransformer>(transformer => transformer == null))) .Returns( async() => { tcs1.TrySetResult(true); await tcs2.Task; }) .Verifiable(); var sut = Create <ProxyInvokerMiddleware>(); Assert.Equal(0, cluster1.ConcurrencyCounter.Value); Assert.Equal(0, destination1.ConcurrentRequestCount); var task = sut.Invoke(httpContext); if (task.IsFaulted) { // Something went wrong, don't hang the test. await task; } Mock <IHttpProxy>().Verify(); await tcs1.Task; // Wait until we get to the proxying step. Assert.Equal(1, cluster1.ConcurrencyCounter.Value); Assert.Equal(1, destination1.ConcurrentRequestCount); Assert.Same(destination1, httpContext.GetReverseProxyFeature().ProxiedDestination); tcs2.TrySetResult(true); await task; Assert.Equal(0, cluster1.ConcurrencyCounter.Value); Assert.Equal(0, destination1.ConcurrentRequestCount); var invoke = Assert.Single(events, e => e.EventName == "ProxyInvoke"); Assert.Equal(3, invoke.Payload.Count); Assert.Equal(cluster1.ClusterId, (string)invoke.Payload[0]); Assert.Equal(routeConfig.ProxyRoute.RouteId, (string)invoke.Payload[1]); Assert.Equal(destination1.DestinationId, (string)invoke.Payload[2]); }
public async Task Invoke( HttpContext context, AuthenticationManager authenticationManager, SingleSignOnHandler singleSignOnHandler, ConfigurationProvider configurationProvider, SecureRandom secureRandom ) { MemorySingletonProxyConfigProvider.Route?route = GetMatchingRoute(context); if (route != null) { bool shouldHandle = true; PathString requestPath = context.Request.Path; if (requestPath.StartsWithSegments("/.well-known/acme-challenge")) { string challenge = ((string)requestPath).Split('/').Last(); if (_acmeChallengeSingleton.Challenges.ContainsKey(challenge)) { shouldHandle = false; } } if (shouldHandle) { configurationProvider.TryGet(AuthServer.Server.GRPC.InstallService.PRIMARY_DOMAIN_KEY, out string primaryDomain); bool isAuthRequest = singleSignOnHandler.IsAuthRequest(context); if (isAuthRequest) { singleSignOnHandler.Handle(context); return; } bool isPublicEndpoint = route.PublicRoutes.Contains(context.Request.Path); if (!isPublicEndpoint) { bool isAuthenticated = authenticationManager.IsAuthenticated(context, out Guid? sessionId); if (!isAuthenticated) { string csrf = secureRandom.GetRandomString(16); context.Response.Cookies.Append("gatekeeper.csrf", csrf); Dictionary <string, string> queryDictionary = new Dictionary <string, string>() { { "id", route.ProxySettingId.ToString() }, { "csrf", csrf }, }; UriBuilder uriBuilder = new UriBuilder(); uriBuilder.Scheme = "https"; uriBuilder.Host = primaryDomain; uriBuilder.Path = "/auth/sso-connect"; uriBuilder.Query = await((new System.Net.Http.FormUrlEncodedContent(queryDictionary)).ReadAsStringAsync()); context.Response.Redirect(uriBuilder.ToString(), false); return; } else { if (sessionId == null) { // This should never happen return; } bool isAuthorized = await authenticationManager.IsAuthorizedAsync((Guid)sessionId, route); if (!isAuthorized) { context.Response.Redirect("https://" + primaryDomain + "/auth/403"); return; } } } RequestProxyOptions proxyOptions = new RequestProxyOptions( TimeSpan.FromSeconds(100), null ); await _httpProxy.ProxyAsync( context, route.InternalHostname, _httpClient, proxyOptions, new Gatekeeper.Server.Web.Services.ReverseProxy.Transformer.RequestTransformer(route) ); return; } } await _nextMiddleware(context); }
public async Task Invoke( HttpContext context, AuthenticationManager authenticationManager, SingleSignOnHandler singleSignOnHandler, ConfigurationProvider configurationProvider, SecureRandom secureRandom ) { MemorySingletonProxyConfigProvider.Route?route = GetMatchingRoute(context); if (route != null) { bool shouldHandle = true; PathString requestPath = context.Request.Path; if (requestPath.StartsWithSegments("/.well-known/acme-challenge")) { string challenge = ((string)requestPath).Split('/').Last(); if (_acmeChallengeSingleton.Challenges.ContainsKey(challenge)) { shouldHandle = false; } } if (shouldHandle) { configurationProvider.TryGet(AuthServer.Server.GRPC.InstallService.PRIMARY_DOMAIN_KEY, out string primaryDomain); bool isAuthRequest = singleSignOnHandler.IsAuthRequest(context); if (isAuthRequest) { singleSignOnHandler.Handle(context); return; } bool isPublicEndpoint = route.PublicRoutes.Contains(context.Request.Path); if (!isPublicEndpoint) { bool isAuthenticated = authenticationManager.IsAuthenticated(context, out Guid? sessionId); if (!isAuthenticated) { string csrf = secureRandom.GetRandomString(16); context.Response.Cookies.Append("gatekeeper.csrf", csrf); Dictionary <string, string> queryDictionary = new Dictionary <string, string>() { { "id", route.ProxySettingId.ToString() }, { "csrf", csrf }, }; UriBuilder uriBuilder = new UriBuilder(); uriBuilder.Scheme = "https"; uriBuilder.Host = primaryDomain; uriBuilder.Path = "/auth/sso-connect"; uriBuilder.Query = await((new System.Net.Http.FormUrlEncodedContent(queryDictionary)).ReadAsStringAsync()); context.Response.Redirect(uriBuilder.ToString(), false); return; } else { if (sessionId == null) { // This should never happen return; } bool isAuthorized = await authenticationManager.IsAuthorizedAsync((Guid)sessionId, route); if (!isAuthorized) { context.Response.Redirect("https://" + primaryDomain + "/auth/403"); return; } } } Dictionary <string, RequestHeaderTransform> requestHeaderTransforms = new Dictionary <string, RequestHeaderTransform>() { { "X-Forwarded-For", new RequestHeaderValueTransform(context.Connection.RemoteIpAddress.ToString(), append: false) }, { "X-Forwarded-Host", new RequestHeaderValueTransform(route.PublicHostname, append: false) }, { HeaderNames.Host, new RequestHeaderValueTransform(String.Empty, append: false) } }; if (context.Request.Cookies.TryGetValue(AuthenticationManager.AUTH_COOKIE, out string?authCookieValue)) { // FIXME: This is currently also sent as cookie. Remove this and only send it as header. requestHeaderTransforms.Add( "X-Gatekeeper-Jwt-Assertion", new RequestHeaderValueTransform(authCookieValue, append: false) ); } RequestProxyOptions proxyOptions = new RequestProxyOptions() { RequestTimeout = TimeSpan.FromSeconds(100), Transforms = new Transforms( copyRequestHeaders: true, requestTransforms: Array.Empty <RequestParametersTransform>(), requestHeaderTransforms: requestHeaderTransforms, responseHeaderTransforms: new Dictionary <string, ResponseHeaderTransform>(), responseTrailerTransforms: new Dictionary <string, ResponseHeaderTransform>() ) }; await _httpProxy.ProxyAsync(context, route.InternalHostname, _httpClient, proxyOptions); return; } } await _nextMiddleware(context); }
public void Equals_Different_Value_Returns_False() { var options1 = new Cluster { Id = "cluster1", Destinations = new Dictionary <string, Destination>(StringComparer.OrdinalIgnoreCase) { { "destinationA", new Destination { Address = "https://localhost:10000/destA", Health = "https://localhost:20000/destA", Metadata = new Dictionary <string, string> { { "destA-K1", "destA-V1" }, { "destA-K2", "destA-V2" } } } }, { "destinationB", new Destination { Address = "https://localhost:10000/destB", Health = "https://localhost:20000/destB", Metadata = new Dictionary <string, string> { { "destB-K1", "destB-V1" }, { "destB-K2", "destB-V2" } } } } }, HealthCheck = new HealthCheckOptions { Passive = new PassiveHealthCheckOptions { Enabled = true, Policy = "FailureRate", ReactivationPeriod = TimeSpan.FromMinutes(5) }, Active = new ActiveHealthCheckOptions { Enabled = true, Interval = TimeSpan.FromSeconds(4), Timeout = TimeSpan.FromSeconds(6), Policy = "Any5xxResponse", Path = "healthCheckPath" } }, LoadBalancingPolicy = LoadBalancingPolicies.Random, SessionAffinity = new SessionAffinityOptions { Enabled = true, FailurePolicy = "Return503Error", Mode = "Cookie", Settings = new Dictionary <string, string> { { "affinity1-K1", "affinity1-V1" }, { "affinity1-K2", "affinity1-V2" } } }, HttpClient = new ProxyHttpClientOptions { SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12, MaxConnectionsPerServer = 10, DangerousAcceptAnyServerCertificate = true, PropagateActivityContext = true, }, HttpRequest = new RequestProxyOptions { Timeout = TimeSpan.FromSeconds(60), Version = Version.Parse("1.0"), #if NET VersionPolicy = HttpVersionPolicy.RequestVersionExact, #endif }, Metadata = new Dictionary <string, string> { { "cluster1-K1", "cluster1-V1" }, { "cluster1-K2", "cluster1-V2" } } }; Assert.False(options1.Equals(options1 with { Id = "different" })); Assert.False(options1.Equals(options1 with { Destinations = new Dictionary <string, Destination>() })); Assert.False(options1.Equals(options1 with { HealthCheck = new HealthCheckOptions() })); Assert.False(options1.Equals(options1 with { LoadBalancingPolicy = "different" })); Assert.False(options1.Equals(options1 with { SessionAffinity = new SessionAffinityOptions { Enabled = true, FailurePolicy = "Return503Error", Mode = "Cookie", Settings = new Dictionary <string, string> { { "affinity1-K1", "affinity1-V1" } } } })); Assert.False(options1.Equals(options1 with { HttpClient = new ProxyHttpClientOptions { SslProtocols = SslProtocols.Tls12, MaxConnectionsPerServer = 10, DangerousAcceptAnyServerCertificate = true, PropagateActivityContext = true, } })); Assert.False(options1.Equals(options1 with { HttpRequest = new RequestProxyOptions() { } })); Assert.False(options1.Equals(options1 with { Metadata = null })); }
/// <summary> /// Proxies the incoming request to the destination server, and the response back to the client. /// </summary> /// <remarks> /// In what follows, as well as throughout in Reverse Proxy, we consider /// the following picture as illustrative of the Proxy. /// <code> /// +-------------------+ /// | Destination + /// +-------------------+ /// ▲ | /// (b) | | (c) /// | ▼ /// +-------------------+ /// | Proxy + /// +-------------------+ /// ▲ | /// (a) | | (d) /// | ▼ /// +-------------------+ /// | Client + /// +-------------------+ /// </code> /// /// (a) and (b) show the *request* path, going from the client to the target. /// (c) and (d) show the *response* path, going from the destination back to the client. /// /// Normal proxying comprises the following steps: /// (0) Disable ASP .NET Core limits for streaming requests /// (1) Create outgoing HttpRequestMessage /// (2) Setup copy of request body (background) Client --► Proxy --► Destination /// (3) Copy request headers Client --► Proxy --► Destination /// (4) Send the outgoing request using HttpMessageInvoker Client --► Proxy --► Destination /// (5) Copy response status line Client ◄-- Proxy ◄-- Destination /// (6) Copy response headers Client ◄-- Proxy ◄-- Destination /// (7-A) Check for a 101 upgrade response, this takes care of WebSockets as well as any other upgradeable protocol. /// (7-A-1) Upgrade client channel Client ◄--- Proxy ◄--- Destination /// (7-A-2) Copy duplex streams and return Client ◄--► Proxy ◄--► Destination /// (7-B) Copy (normal) response body Client ◄-- Proxy ◄-- Destination /// (8) Copy response trailer headers and finish response Client ◄-- Proxy ◄-- Destination /// (9) Wait for completion of step 2: copying request body Client --► Proxy --► Destination /// /// ASP .NET Core (Kestrel) will finally send response trailers (if any) /// after we complete the steps above and relinquish control. /// </remarks> public async Task ProxyAsync( HttpContext context, string destinationPrefix, HttpMessageInvoker httpClient, RequestProxyOptions requestOptions, HttpTransformer transformer) { _ = context ?? throw new ArgumentNullException(nameof(context)); _ = destinationPrefix ?? throw new ArgumentNullException(nameof(destinationPrefix)); _ = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); transformer ??= HttpTransformer.Default; // HttpClient overload for SendAsync changes response behavior to fully buffered which impacts performance // See discussion in https://github.com/microsoft/reverse-proxy/issues/458 if (httpClient is HttpClient) { throw new ArgumentException($"The http client must be of type HttpMessageInvoker, not HttpClient", nameof(httpClient)); } ProxyTelemetry.Log.ProxyStart(destinationPrefix); try { var requestAborted = context.RequestAborted; var isClientHttp2 = ProtocolHelper.IsHttp2(context.Request.Protocol); // NOTE: We heuristically assume gRPC-looking requests may require streaming semantics. // See https://github.com/microsoft/reverse-proxy/issues/118 for design discussion. var isStreamingRequest = isClientHttp2 && ProtocolHelper.IsGrpcContentType(context.Request.ContentType); // :: Step 1-3: Create outgoing HttpRequestMessage var(destinationRequest, requestContent) = await CreateRequestMessageAsync( context, destinationPrefix, transformer, requestOptions, isStreamingRequest, requestAborted); // :: Step 4: Send the outgoing request using HttpClient HttpResponseMessage destinationResponse; var requestTimeoutSource = CancellationTokenSource.CreateLinkedTokenSource(requestAborted); requestTimeoutSource.CancelAfter(requestOptions?.Timeout ?? DefaultTimeout); var requestTimeoutToken = requestTimeoutSource.Token; try { ProxyTelemetry.Log.ProxyStage(ProxyStage.SendAsyncStart); destinationResponse = await httpClient.SendAsync(destinationRequest, requestTimeoutToken); ProxyTelemetry.Log.ProxyStage(ProxyStage.SendAsyncStop); } catch (OperationCanceledException canceledException) { if (!requestAborted.IsCancellationRequested && requestTimeoutToken.IsCancellationRequested) { ReportProxyError(context, ProxyError.RequestTimedOut, canceledException); context.Response.StatusCode = StatusCodes.Status504GatewayTimeout; return; } ReportProxyError(context, ProxyError.RequestCanceled, canceledException); context.Response.StatusCode = StatusCodes.Status502BadGateway; return; } catch (Exception requestException) { await HandleRequestFailureAsync(context, requestContent, requestException); return; } finally { requestTimeoutSource.Dispose(); } // Detect connection downgrade, which may be problematic for e.g. gRPC. if (isClientHttp2 && destinationResponse.Version.Major != 2) { // TODO: Do something on connection downgrade... Log.HttpDowngradeDetected(_logger); } try { // :: Step 5: Copy response status line Client ◄-- Proxy ◄-- Destination // :: Step 6: Copy response headers Client ◄-- Proxy ◄-- Destination var copyBody = await CopyResponseStatusAndHeadersAsync(destinationResponse, context, transformer); if (!copyBody) { // The transforms callback decided that the response body should be discarded. destinationResponse.Dispose(); return; } } catch (Exception ex) { destinationResponse.Dispose(); ReportProxyError(context, ProxyError.ResponseHeaders, ex); // Clear the response since status code, reason and some headers might have already been copied and we want clean 502 response. context.Response.Clear(); context.Response.StatusCode = StatusCodes.Status502BadGateway; return; } // :: Step 7-A: Check for a 101 upgrade response, this takes care of WebSockets as well as any other upgradeable protocol. if (destinationResponse.StatusCode == HttpStatusCode.SwitchingProtocols) { await HandleUpgradedResponse(context, destinationResponse, requestAborted); return; } // NOTE: it may *seem* wise to call `context.Response.StartAsync()` at this point // since it looks like we are ready to send back response headers // (and this might help reduce extra delays while we wait to receive the body from the destination). // HOWEVER, this would produce the wrong result if it turns out that there is no content // from the destination -- instead of sending headers and terminating the stream at once, // we would send headers thinking a body may be coming, and there is none. // This is problematic on gRPC connections when the destination server encounters an error, // in which case it immediately returns the response headers and trailing headers, but no content, // and clients misbehave if the initial headers response does not indicate stream end. // :: Step 7-B: Copy response body Client ◄-- Proxy ◄-- Destination var(responseBodyCopyResult, responseBodyException) = await CopyResponseBodyAsync(destinationResponse.Content, context.Response.Body, requestAborted); if (responseBodyCopyResult != StreamCopyResult.Success) { await HandleResponseBodyErrorAsync(context, requestContent, responseBodyCopyResult, responseBodyException); return; } // :: Step 8: Copy response trailer headers and finish response Client ◄-- Proxy ◄-- Destination await CopyResponseTrailingHeadersAsync(destinationResponse, context, transformer); if (isStreamingRequest) { // NOTE: We must call `CompleteAsync` so that Kestrel will flush all bytes to the client. // In the case where there was no response body, // this is also when headers and trailing headers are sent to the client. // Without this, the client might wait forever waiting for response bytes, // while we might wait forever waiting for request bytes, // leading to a stuck connection and no way to make progress. await context.Response.CompleteAsync(); } // :: Step 9: Wait for completion of step 2: copying request body Client --► Proxy --► Destination // NOTE: It is possible for the request body to NOT be copied even when there was an incoming requet body, // e.g. when the request includes header `Expect: 100-continue` and the destination produced a non-1xx response. // We must only wait for the request body to complete if it actually started, // otherwise we run the risk of waiting indefinitely for a task that will never complete. if (requestContent != null && requestContent.Started) { var(requestBodyCopyResult, requestBodyException) = await requestContent.ConsumptionTask; if (requestBodyCopyResult != StreamCopyResult.Success) { // The response succeeded. If there was a request body error then it was probably because the client or destination decided // to cancel it. Report as low severity. var error = requestBodyCopyResult switch { StreamCopyResult.InputError => ProxyError.RequestBodyClient, StreamCopyResult.OutputError => ProxyError.RequestBodyDestination, StreamCopyResult.Canceled => ProxyError.RequestBodyCanceled, _ => throw new NotImplementedException(requestBodyCopyResult.ToString()) }; ReportProxyError(context, error, requestBodyException); } } } finally { ProxyTelemetry.Log.ProxyStop(context.Response.StatusCode); } }
private async ValueTask <(HttpRequestMessage, StreamCopyHttpContent)> CreateRequestMessageAsync(HttpContext context, string destinationPrefix, HttpTransformer transformer, RequestProxyOptions requestOptions, bool isStreamingRequest, CancellationToken requestAborted) { // "http://a".Length = 8 if (destinationPrefix == null || destinationPrefix.Length < 8) { throw new ArgumentException(nameof(destinationPrefix)); } var destinationRequest = new HttpRequestMessage(); destinationRequest.Method = HttpUtilities.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 : (requestOptions?.Version ?? DefaultVersion); #if NET destinationRequest.VersionPolicy = isUpgradeRequest ? HttpVersionPolicy.RequestVersionOrLower : (requestOptions?.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, requestAborted); destinationRequest.Content = requestContent; // :: Step 3: Copy request headers Client --► Proxy --► Destination await transformer.TransformRequestAsync(context, destinationRequest, destinationPrefix); // 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.RequestUri); // TODO: What if they replace the HttpContent object? That would mess with our tracking and error handling. return(destinationRequest, requestContent); }