public Task <bool> Handle(HttpContext context, SessionAffinityConfig config, AffinityStatus affinityStatus) { if (affinityStatus == AffinityStatus.OK || affinityStatus == AffinityStatus.AffinityKeyNotSet) { throw new InvalidOperationException($"{nameof(Return503ErrorAffinityFailurePolicy)} is called to handle a successful request's affinity status {affinityStatus}."); } context.Response.StatusCode = 503; return(TaskUtilities.FalseTask); }
public Task <bool> Handle(HttpContext context, SessionAffinityConfig config, AffinityStatus affinityStatus) { if (affinityStatus == AffinityStatus.OK || affinityStatus == AffinityStatus.AffinityKeyNotSet) { throw new InvalidOperationException($"{nameof(RedistributeAffinityFailurePolicy)} is called to handle a successful request's affinity status {affinityStatus}."); } // Available destinations list have not been changed in the context, // so simply allow processing to proceed to load balancing. return(TaskUtilities.TrueTask); }
public void FindAffinitizedDestination_AffinityDisabledOnCluster_ReturnsAffinityDisabled() { var provider = new ProviderStub(GetDataProtector().Object, AffinityTestHelper.GetLogger <BaseSessionAffinityPolicy <string> >().Object); var options = new SessionAffinityConfig { Enabled = false, Policy = _defaultOptions.Policy, FailurePolicy = _defaultOptions.FailurePolicy, AffinityKeyName = _defaultOptions.AffinityKeyName }; var cluster = new ClusterState("cluster"); Assert.Throws <InvalidOperationException>(() => provider.FindAffinitizedDestinations(new DefaultHttpContext(), cluster, options, new[] { new DestinationState("1") })); }
public void Equals_Second_Null_Returns_False() { var options1 = new SessionAffinityConfig { Enabled = true, FailurePolicy = "policy1", Policy = "policy1", AffinityKeyName = "Key1" }; var equals = options1.Equals(null); Assert.False(equals); }
public virtual void AffinitizeRequest(HttpContext context, SessionAffinityConfig config, DestinationState destination) { if (!config.Enabled.GetValueOrDefault()) { throw new InvalidOperationException($"Session affinity is disabled for cluster."); } // Affinity key is set on the response only if it's a new affinity. if (!context.Items.ContainsKey(AffinityKeyId)) { var affinityKey = GetDestinationAffinityKey(destination); SetAffinityKey(context, config, affinityKey); } }
protected override void SetAffinityKey(HttpContext context, SessionAffinityConfig config, string unencryptedKey) { var affinityCookieOptions = new CookieOptions { Path = config.Cookie?.Path ?? "/", SameSite = config.Cookie?.SameSite ?? SameSiteMode.Unspecified, HttpOnly = config.Cookie?.HttpOnly ?? true, MaxAge = config.Cookie?.MaxAge, Domain = config.Cookie?.Domain, IsEssential = config.Cookie?.IsEssential ?? false, Secure = config.Cookie?.SecurePolicy == CookieSecurePolicy.Always || (config.Cookie?.SecurePolicy == CookieSecurePolicy.SameAsRequest && context.Request.IsHttps), Expires = config.Cookie?.Expiration != null?_clock.GetUtcNow().Add(config.Cookie.Expiration.Value) : default(DateTimeOffset?), }; context.Response.Cookies.Append(config.AffinityKeyName, Protect(unencryptedKey), affinityCookieOptions); }
public void Equals_Different_Value_Returns_False() { var options1 = new SessionAffinityConfig { Enabled = true, FailurePolicy = "policy1", Policy = "policy1", AffinityKeyName = "Key1" }; var options2 = new SessionAffinityConfig { Enabled = false, FailurePolicy = "policy2", Policy = "policy2", AffinityKeyName = "Key1" }; var equals = options1.Equals(options2); Assert.False(equals); }
public void Equals_Same_Value_Returns_True() { var options1 = new SessionAffinityConfig { Enabled = true, FailurePolicy = "policy1", Policy = "policy1", AffinityKeyName = "Key1" }; var options2 = new SessionAffinityConfig { Enabled = true, FailurePolicy = "Policy1", Policy = "Policy1", AffinityKeyName = "Key1" }; var equals = options1.Equals(options2); Assert.True(equals); Assert.Equal(options1.GetHashCode(), options2.GetHashCode()); }
private async Task InvokeInternal(HttpContext context, IReverseProxyFeature proxyFeature, SessionAffinityConfig config) { var destinations = proxyFeature.AvailableDestinations; var cluster = proxyFeature.Route.Cluster !; var policy = _sessionAffinityPolicies.GetRequiredServiceById(config.Policy, SessionAffinityConstants.Policies.Cookie); var affinityResult = policy.FindAffinitizedDestinations(context, cluster, config, destinations); switch (affinityResult.Status) { case AffinityStatus.OK: proxyFeature.AvailableDestinations = affinityResult.Destinations !; break; case AffinityStatus.AffinityKeyNotSet: // Nothing to do so just continue processing break; case AffinityStatus.AffinityKeyExtractionFailed: case AffinityStatus.DestinationNotFound: var failurePolicy = _affinityFailurePolicies.GetRequiredServiceById(config.FailurePolicy, SessionAffinityConstants.FailurePolicies.Redistribute); var keepProcessing = await failurePolicy.Handle(context, proxyFeature.Route.Cluster !, affinityResult.Status); if (!keepProcessing) { // Policy reported the failure is unrecoverable and took the full responsibility for its handling, // so we simply stop processing. Log.AffinityResolutionFailedForCluster(_logger, cluster.ClusterId); return; } Log.AffinityResolutionFailureWasHandledProcessingWillBeContinued(_logger, cluster.ClusterId, failurePolicy.Name); break; default: throw new NotSupportedException($"Affinity status '{affinityResult.Status}' is not supported."); } await _next(context); }
protected override (string?Key, bool ExtractedSuccessfully) GetRequestAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig config) { var encryptedRequestKey = context.Request.Cookies.TryGetValue(config.AffinityKeyName, out var keyInCookie) ? keyInCookie : null; return(Unprotect(encryptedRequestKey)); }
protected abstract void SetAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig config, T unencryptedKey);
protected abstract (T?Key, bool ExtractedSuccessfully) GetRequestAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig config);
public virtual AffinityResult FindAffinitizedDestinations(HttpContext context, ClusterState cluster, SessionAffinityConfig config, IReadOnlyList <DestinationState> destinations) { if (!config.Enabled.GetValueOrDefault()) { throw new InvalidOperationException($"Session affinity is disabled for cluster {cluster.ClusterId}."); } var requestAffinityKey = GetRequestAffinityKey(context, cluster, config); if (requestAffinityKey.Key == null) { return(new AffinityResult(null, requestAffinityKey.ExtractedSuccessfully ? AffinityStatus.AffinityKeyNotSet : AffinityStatus.AffinityKeyExtractionFailed)); } IReadOnlyList <DestinationState>?matchingDestinations = null; if (destinations.Count > 0) { for (var i = 0; i < destinations.Count; i++) { // TODO: Add fast destination lookup by ID if (requestAffinityKey.Key.Equals(GetDestinationAffinityKey(destinations[i]))) { // It's allowed to affinitize a request to a pool of destinations so as to enable load-balancing among them. // However, we currently stop after the first match found to avoid performance degradation. matchingDestinations = destinations[i]; break; } } if (matchingDestinations == null) { Log.DestinationMatchingToAffinityKeyNotFound(Logger, cluster.ClusterId); } } else { Log.AffinityCannotBeEstablishedBecauseNoDestinationsFound(Logger, cluster.ClusterId); } // Empty destination list passed to this method is handled the same way as if no matching destinations are found. if (matchingDestinations == null) { return(new AffinityResult(null, AffinityStatus.DestinationNotFound)); } context.Items[AffinityKeyId] = requestAffinityKey; return(new AffinityResult(matchingDestinations, AffinityStatus.OK)); }
public void Equals_Different_Value_Returns_False() { var config1 = new ClusterConfig { ClusterId = "cluster1", Destinations = new Dictionary <string, DestinationConfig>(StringComparer.OrdinalIgnoreCase) { { "destinationA", new DestinationConfig { 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 DestinationConfig { 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 HealthCheckConfig { Passive = new PassiveHealthCheckConfig { Enabled = true, Policy = "FailureRate", ReactivationPeriod = TimeSpan.FromMinutes(5) }, Active = new ActiveHealthCheckConfig { Enabled = true, Interval = TimeSpan.FromSeconds(4), Timeout = TimeSpan.FromSeconds(6), Policy = "Any5xxResponse", Path = "healthCheckPath" } }, LoadBalancingPolicy = LoadBalancingPolicies.Random, SessionAffinity = new SessionAffinityConfig { Enabled = true, FailurePolicy = "Return503Error", Policy = "Cookie", AffinityKeyName = "Key1", Cookie = new SessionAffinityCookieConfig { Domain = "localhost", Expiration = TimeSpan.FromHours(3), HttpOnly = true, IsEssential = true, MaxAge = TimeSpan.FromDays(1), Path = "mypath", SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict, SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.SameAsRequest } }, HttpClient = new HttpClientConfig { SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12, MaxConnectionsPerServer = 10, DangerousAcceptAnyServerCertificate = true, ActivityContextHeaders = ActivityContextHeaders.CorrelationContext, }, HttpRequest = new ForwarderRequestConfig { 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(config1.Equals(config1 with { ClusterId = "different" })); Assert.False(config1.Equals(config1 with { Destinations = new Dictionary <string, DestinationConfig>() })); Assert.False(config1.Equals(config1 with { HealthCheck = new HealthCheckConfig() })); Assert.False(config1.Equals(config1 with { LoadBalancingPolicy = "different" })); Assert.False(config1.Equals(config1 with { SessionAffinity = new SessionAffinityConfig { Enabled = true, FailurePolicy = "Return503Error", Policy = "Cookie", AffinityKeyName = "Key1", Cookie = new SessionAffinityCookieConfig { Domain = "localhost", Expiration = TimeSpan.FromHours(3), HttpOnly = true, IsEssential = true, MaxAge = TimeSpan.FromDays(1), Path = "newpath", SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict, SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.SameAsRequest } } })); Assert.False(config1.Equals(config1 with { HttpClient = new HttpClientConfig { SslProtocols = SslProtocols.Tls12, MaxConnectionsPerServer = 10, DangerousAcceptAnyServerCertificate = true, ActivityContextHeaders = ActivityContextHeaders.CorrelationContext, } })); Assert.False(config1.Equals(config1 with { HttpRequest = new ForwarderRequestConfig() { } })); Assert.False(config1.Equals(config1 with { Metadata = null })); }
protected override void SetAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig config, string unencryptedKey) { context.Response.Headers.Append(config.AffinityKeyName, Protect(unencryptedKey)); }
protected override (string?Key, bool ExtractedSuccessfully) GetRequestAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig config) { var customHeaderName = config.AffinityKeyName; var keyHeaderValues = context.Request.Headers[customHeaderName]; if (StringValues.IsNullOrEmpty(keyHeaderValues)) { // It means affinity key is not defined that is a successful case return(Key : null, ExtractedSuccessfully : true); } if (keyHeaderValues.Count > 1) { // Multiple values is an ambiguous case which is considered a key extraction failure Log.RequestAffinityHeaderHasMultipleValues(Logger, customHeaderName, keyHeaderValues.Count); return(Key : null, ExtractedSuccessfully : false); } return(Unprotect(keyHeaderValues[0])); }