Monday, September 18, 2017

Kentico behind Reverse proxy

The website I am developing using Kentico EMS is running on a server behind a reverse proxy'ing firewall. This firewall takes care of the SSL communications (called SSL Offloading). This means that the proxy handles the SSL, and Kentico only sees HTTP requests.

Because requests in Kentico are always coming from the firewall and all request are always HTTP this causes several problems for Kentico:
  1. Functionalities like IP Banning and GeoLocation for our contacts are not working because Kentico only sees the proxy servers IP Address.
  2. Because Kentico only receives HTTP requests, all links created by Kentico will will be http links. This causes additional redirects when clients request images or files from Kentico (which will be redirected to https again). Also browsers may complain about 'mixed content'
  3. Because all requests in Kentico arrive non-SSL, setting the 'Requires SSL' option on a page to 'Yes' causes a redirect loop.

The Solution

Most reverse proxy firewalls can add the X-Forwarded-For and x-Forwarded_Proto headers to the request making it possible to detect the orignal Ip address and the original protocol that arrived at the proxy. Using this information we can use the 'RequestEvents.Prepare.Execute' event in Kentico to fix the RequestContext.

The following example code does exacly this:
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
using System;
using System.Web;
using System.Linq;
using CMS;
using CMS.DataEngine;
using CMS.Base;
using CMS.EventLog;
using CMS.Helpers;

// Registers the custom module into the system
[assembly: RegisterModule(typeof(ReverseProxyModule))]
class ReverseProxyModule : Module
{

    // Module class constructor, the system registers the module under the name "ReverseProxyModule"
    public ReverseProxyModule()
        : base("ReverseProxy")
    {
    }

    /// <summary>
    /// Initialize the module. Bind to the corect event handler to handle reverse proxy requests
    /// </summary>
    protected override void OnInit()
    {
        base.OnInit();

        // Assigns a handler called before each request is processed
        RequestEvents.Prepare.Execute += HandleReverseProxyRequests;
    }
    
    /// <summary>
    /// If Kentico is behind a reverse proxy, fix SSL and UserHostAddress settings in the context
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private static void HandleReverseProxyRequests(object sender, EventArgs e)
    {
        if (HttpContext.Current != null)
        {
            HandleUserHostAddress();
            HandleIsSsl();

            LogHeaders();
        }
    }

    private static void LogHeaders()
    {
        try
        {
            if (RequestContext.CurrentQueryString.Contains("logheaders"))
            {
                var values = HttpContext.Current.Request.Headers.AllKeys.Select(e => $"{e} = {HttpContext.Current.Request.Headers[e]}"); 
                EventLogProvider.LogInformation("ReverseProxyModule", "LogHeaders", string.Join("<br/>", values));
            }
        }
        catch (Exception ex)
        {
            EventLogProvider.LogException("ReverseProxyModule", "LogHeaders", ex);
        }
    }

    /// <summary>
    /// Handles if the request was forwarded from a SSL Offloading proxy
    /// </summary>
    private static void HandleIsSsl()
    {
        try
        {
            // Gets the value from the X-Forwarded-Ssl header
            var xForwardedProto = HttpContext.Current.Request.Headers.Get("X-Forwarded-Proto");
            RequestContext.IsSSL = false;

            // Checks if the original request used HTTPS
            if (string.Equals(xForwardedProto, "https", StringComparison.OrdinalIgnoreCase))
            {
                RequestContext.IsSSL = true;
                URLHelper.SSLUrlPort = 443;
            }
        }
        catch (Exception ex)
        {
            EventLogProvider.LogException("ReverseProxyModule", "HandleIsSsl", ex);
        }
    }

    /// <summary>
    /// Provides Kentico with the correct Host Address if the original address is hidden by using a Reverse proxy
    /// </summary>
    private static void HandleUserHostAddress()
    {
        try
        {
            // Gets the value from the X-forwarded-for header and pass it to the request context
            var xForwardFor = HttpContext.Current.Request.Headers.Get("X-Forwarded-For");
            
            if (!string.IsNullOrEmpty(xForwardFor))
                RequestContext.UserHostAddress = xForwardFor.Split(',')[0];
        }
        catch (Exception ex)
        {
            EventLogProvider.LogException("ReverseProxyModule", "HandleUserHostAddress", ex);
        }
    }
}

Also note the method 'LogHeaders' which ouputs all headers to the EventLog if the 'logheaders' querystring parameter is present. This proved to be usefull when debugging the headers and checking of the X-Forward-* headers are added by the proxy or not.


1 comment:

Chasing the Sun said...

Hi thanks for posting thiss