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") }));
    }
Esempio n. 4
0
    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);
    }
Esempio n. 5
0
        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);
            }
        }
Esempio n. 6
0
        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);
        }
Esempio n. 7
0
    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);
    }
Esempio n. 8
0
    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());
    }
Esempio n. 9
0
    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);
    }
Esempio n. 10
0
    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));
        }
Esempio n. 14
0
        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
            }));
        }
Esempio n. 15
0
 protected override void SetAffinityKey(HttpContext context, ClusterState cluster, SessionAffinityConfig config, string unencryptedKey)
 {
     context.Response.Headers.Append(config.AffinityKeyName, Protect(unencryptedKey));
 }
Esempio n. 16
0
    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]));
    }