コード例 #1
0
ファイル: Startup.cs プロジェクト: stantoxt/reverse-proxy
        /// <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;
                    }
                });
            });
        }
コード例 #2
0
        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);
        }
コード例 #3
0
        /// <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();
            }
        }
コード例 #4
0
ファイル: Startup.cs プロジェクト: bcc-code/bcc-wp-proxy
        // 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();
            });
        }
コード例 #5
0
        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);
        }
コード例 #6
0
 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;
 }
コード例 #7
0
        // 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;
                    }
                });
            });
        }
コード例 #8
0
        /// <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;
                    }
                });
            });
        }
コード例 #9
0
        /// <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;
                    }
                });
            });
        }
コード例 #10
0
        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);
        }
コード例 #11
0
        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>());
コード例 #12
0
ファイル: Startup.cs プロジェクト: vancechan/reverse-proxy
        /// <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;
                    }
                });
            });
        }
コード例 #13
0
        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]);
        }
コード例 #14
0
        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);
        }
コード例 #15
0
        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);
        }
コード例 #16
0
        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
            }));
        }
コード例 #17
0
        /// <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);
            }
        }
コード例 #18
0
        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);
        }