Visitor Groups criteria with load balanced env

Vote:
 

Hello

We use episerver CMS site 10.5 version. 

There are configured visitor groups criteria content (user IP criteria) - block with settings which are used in custom frontend widget (some booking widget).

So users from different Ip's see different settings. It works fine on our DEV env. But on PROD a load balancer (AWS hosting) is configured.

We have several versions of Web.config and transformations. On Web.Production.config is set specific parameter to use header for load balanced configurations:  

<appSettings>
<add key="episerver:ClientIPAddressHeader" value="X-Forwarded-For" xdt:Transform="InsertIfMissing" xdt:Locator="Match(key)"/>
</appSettings>

Also custom episerver Ip resolver was injected:

ClientIpAddressResolver : IClientIPAddressResolver

I added to the custom IP resolver traces and logging and see that method ResolveAddress returns correct IP address of user (not a load balancer IP) even on PROD site.

Also tried to define explicitly header with 'right' Ip address on Custom Ip resolver (to exclude version that web.config is not affects): 

private const string HeaderName = "X-Forwarded-For";

But visitor group content still not working. 

Another version is a caching on hosting side but AWS support says that there is no specific cache for PROD site. 

#207651
Edited, Sep 27, 2019 20:18
Vote:
 

Have you checked what value is passed as the X-Forwarded-For value? If contains multiple values then you may need to configure the episerver:ClientIPAddressProxyCount value as described here:

https://world.episerver.com/documentation/developer-guides/CMS/Deployment/content-delivery-network-cdn-configuration/

#207653
Sep 27, 2019 14:33
Vote:
 

Hi Mikhail,

IP address isn't one of the inbuilt visitor group criteria so would be unlikely to use any of the mechanisms you mention above (those would be for the IP geolocation criteria). Is your IP address criterion something custom written or is it the IPRangeCriterion from the Episerver Criteria Pack nuget package? Either way, I think you'll need to modify the criterion yourself to take the CDN/proxy into consideration and retrieve the IP address from the list in X-Forwarded-For.

#207654
Sep 27, 2019 14:39
Vote:
 

Thanks Paul, I just read IP and thought it was the built in country look up :)! Mikhail - ignore my previous post!

You can add some config in IIS and use the IISUrlRewrite module to use the X-Forwarded-For header as the REMOTE_ADDR header which is I guess what the IP criteria uses. I wrote some blogs about this previosly: 

https://www.david-tec.com/2011/06/Using-the-IIS-rewrite-module-to-test-EPiServer-geo-IP-look-up-personalisation/

https://www.david-tec.com/2011/07/Ensure-EPiServer-Geo-IP-personalisation-works-when-using-Akamai/

This would mean you can make this work with some config changes instead of modifying the code of the criteria.

David

#207655
Sep 27, 2019 14:52
Vote:
 

Thanks for response

Paul Gruffydd, Yes, EPiServer.VisitorGroupsCriteriaPack is installed and 'Technical criteria' -> 'IP range' is configured and works fine on DEV env.

Custom IP resolver is used:

    public class ClientIpAddressResolver : IClientIPAddressResolver
    {
        private const string HeaderName = "X-Forwarded-For";
        private static readonly ILogger Logger = LogManager.GetLogger(typeof(ClientIpAddressResolver));
        
        public int ClientIpAddressProxyCount => 1;

        public IPAddress ResolveAddress(HttpContextBase httpContext)
        {
            IPAddress address;
            if (!IPAddress.TryParse(httpContext.Request.UserHostAddress, out address))
                address = IPAddress.None;
            
            Logger.Error(string.Format("UserHostAddress: '{0}'", address));
            Logger.Error(string.Format("From appSettings: '{0}'", 
                ConfigurationManager.AppSettings["episerver:ClientIPAddressHeader"]));
            Logger.Error(string.Format("Forwarded from headers: '{0}'", httpContext.Request.Headers[HeaderName]));
            
            if (!string.IsNullOrEmpty(HeaderName))
            {
                string header = httpContext.Request.Headers[HeaderName];
                Logger.Error(string.Format("Header: '{0}'", header));
                if (!string.IsNullOrEmpty(header))
                {
                    string[] ipHeaderParts = header.Split(',');
                    address = this.GetUserIPFromHeader(address, ipHeaderParts, this.ClientIpAddressProxyCount) ?? address;
                }
            }

            Logger.Error(string.Format("Address before returning: '{0}' ", address));
            return address;
        }

        private IPAddress GetUserIPFromHeader(IPAddress connectingIP, string[] ipHeaderParts, int proxyCount)
        {
            if (proxyCount > ipHeaderParts.Length)
                proxyCount = ipHeaderParts.Length;
            for (int index = ipHeaderParts.Length - 1 - (proxyCount - 1); index >= 0; --index)
            {
                IPAddress ip;
                if (this.TryParseIPWithPort(ipHeaderParts[index], out ip) && !ip.Equals((object)connectingIP))
                    return ip;
            }
            return (IPAddress)null;
        }

        private bool TryParseIPWithPort(string ipString, out IPAddress ip)
        {
            string[] strArray = ipString.Split(':');
            if (strArray.Length == 2)
                ipString = strArray[0];
            return IPAddress.TryParse(ipString.Trim(), out ip);
        }
    }

On the log I can see the following:

.Epi.DI.ClientIpAddressResolver: UserHostAddress: 'xx.xx.xx.xx'
.Epi.DI.ClientIpAddressResolver: From appSettings: 'X-Forwarded-For'
.Epi.DI.ClientIpAddressResolver: Forwarded from headers: 'x.x.x.x'
.Epi.DI.ClientIpAddressResolver: Header: 'x.x.x.x'
.Epi.DI.ClientIpAddressResolver: Address before returning: 'x.x.x.x'

where:
xx.xx.xx.xx - balancer/proxy address
x.x.x.x - real client address

It looks like it returns correct Ip address of real user.

David Knipe, in my case it is easier to make changes on code not a configure IIS and other deploy stuff. Cause the site is hosted on AWS and access to source code is granted only.

As I understand there is no differences to use 'X-Forwarded-For' or 'REMOTE_ADDR' in my case, cause from logs I can see that X-Forwarded-For is populated with correct address.

#207657
Sep 27, 2019 15:21
Vote:
 

Looking at the source code of the IPRangeCriterion, it looks at the IP address directly rather than via the IClientIPAddressResolver:

var addr = httpContext.Request["TestIP"] ?? httpContext.Request.UserHostAddress;

This means that you'd need to either take an approach along the lines of what David suggested and modify the UserHostAddress server variable early on in the request or create a modified version of the IPRangeCriterion which uses your IClientIPResolver along the lines of this:

[VisitorGroupCriterion(
    Category = "Technical Criteria",
    DisplayName = "IPRange",
    Description = "Criterion that matches certain IPv4 ranges")]
public class IPRangeCriterion : CriterionBase<IPRangeModel>
{
    public override bool IsMatch(System.Security.Principal.IPrincipal principal, System.Web.HttpContextBase httpContext)
    {
        if (httpContext.Request == null) return false;
        var clientIpAddressResolver = ServiceLocator.Current.GetInstance<IClientIPAddressResolver>();
        var addr = clientIpAddressResolver.ResolveAddress(httpContext).ToString();
        
        if (Model.Condition == IPCompareCondition.Equal) return addr == Model.IP;

        var p1 = addr.Split('.').Select(s => int.Parse(s)).ToArray();
        var p2 = Model.IP.Split('.').Select(s => int.Parse(s)).ToArray();

        for (int i = 0; i < 4; i++)
        {
            if (p1[i] == p2[i]) continue;
            if((Model.Condition == IPCompareCondition.GT) && (p1[i]<p2[i]))    return false;
            if((Model.Condition == IPCompareCondition.LT) && (p1[i]>p2[i])) return false;
        }
        return true;

    }
}

You could then use that version instead of the version from the package either by removing the nuget package, replacing the criterion from the package using dependency injection or creating it under a different name.

#207658
Sep 27, 2019 17:39
Vote:
 

David Knipe, I did not have any chance to check configuration on server side.

Paul Gruffydd, this approach works like a charm. Very thanks. 

#208554
Oct 28, 2019 8:45
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.