The Problem

My deployment architecture of choice (and the one I used for the Treeloop website), is an ASP.NET Core application in a Docker container deployed to a cluster of Linux instances, behind a load balancer. All of this happens with the help of the Amazon EC2 AWS Container Service (ECS). The load balancer (ELB) provided by AWS does a great job of terminating HTTPS connections and forwarding requests on to Kestrel running in the Docker containers. The issue I ran into is how to force HTTPS (redirect HTTP requests to HTTPS). In the "old days", I would be deploying to IIS and would use UrlRewrite to do this job.

A Possible Solution

With ASP.NET Core 1.1 came something new - URL Rewriting Middleware. This was designed to fill the gap left by UrlRewrite and can even be configured using IIS standard XML formatted rules or Apache Mod_Rewrite syntax. Wait, there's more... there are also some built-in rules and associated extension methods for some common scenarios including AddRedirectToHttps().

Sadness

Unfortunately, AddRedirectToHttps() won't quite work in my load balanced scenario. The load balancer forwards all requests to Kestrel on port 80 (HTTP). So, the URL Rewriting Middleware will never see the request as HTTPS. If I had full control over the load balancer (e.g. my own Nginx instance), I could have it do the job for me but that is not possible in my case. The silver lining is that it's easy to extend the URL Rewriting Middleware with your own custom rules.

The Solution

Step one was to look at the source code for the existing RedirectToHttpsRule class. Open source is great. I used that as the starting point for my own custom rule.

public class RedirectToProxiedHttpsRule : IRule  
{
    public virtual void ApplyRule(RewriteContext context)
    {
        var request = context.HttpContext.Request;
        var response = context.HttpContext.Response;

        // #1) Did this request start off as HTTP?
        // #2) If so, redirect to HTTPS equivalent
    }
}

For #1 above, I learned from the AWS documentation that the load balancer will add an X-Forwarded-Proto header to the request when it forwards it on to Kestrel.

string reqProtocol;  
if (request.Headers.ContainsKey("X-Forwarded-Proto")) {  
    reqProtocol = request.Headers["X-Forwarded-Proto"][0];
} else {
    reqProtocol = (request.IsHttps ? "https" : "http");
}

For #2, it was just a matter of constructing the right URL and performing the redirect.

if (reqProtocol != "https") {  
    var newUrl = new StringBuilder()
        .Append("https://").Append(request.Host)
        .Append(request.PathBase).Append(request.Path)
        .Append(request.QueryString);

    context.HttpContext.Response.Redirect(newUrl.ToString(), true);
}  

For a little bit of polish, I would like to be able to add my custom rule to the URL Rewriting Middleware in the same way the built-in RedirectToHttpsRule is added. For this, I just needed to add an extension method.

public static class RedirectToProxiedHttpsExtensions  
{
    public static RewriteOptions AddRedirectToProxiedHttps(this RewriteOptions options)
    {
        options.Rules.Add(new RedirectToProxiedHttpsRule());
        return options;
    }
}

Finally, back in the Configure method of Startup.cs...

if (env.IsProduction()) {  
    var options = new RewriteOptions()
        .AddRedirectToProxiedHttps()
        .AddRedirect("(.*)/$", "$1");  // remove trailing slash

    app.UseRewriter(options);
}

Debugging Tip - One issue I ran into when debugging my new rule was that the web browser (Chrome in my case) really liked to cache URLs that it got with a 301 redirect. Fortunately, a setting in Chrome's developer tools can help resolve that issue:

Chrome Developer Tools