A Custom “RequireHttpsAttribute” for Use With AppHarbor

Further to my previous article, here’s how I handle one of the common code quirks of AppHarbor web hosting

In my article on AppHarbor gotchas I promised that I would detail my own method for handling the critical issue that the [RequireHttpsAttribute()] simply won’t work on their network of load balanced servers. By way of introduction I’ll quote from that article:

You’ll have to make several concessions in your code to the load balancer, accepting that it needs to do its thing in a certain way. Because of the manner in which web requests get internally routed through AppHarbor’s system of servers you’ll need to stop using the controller action attribute [RequireHttpsAttribute()], as this won’t work correctly; once the request has passed beyond the edge server it is stripped of its HTTPS distinction, and thus internally the controller action can’t be identified if this attribute is attached to it.

The solution – well, my solution at least – is a custom attribute with which you can decorate your secured actions, which I’m calling [RequireHttpsRemixAttribute()]. You attach it in exactly the same way as the regular HTTPS-enforcement attribute, but inside it’s doing some subtly different checks to confirm the specifics of the HTTP request. Here’s the code:

/// <summary>
/// Custom implementation of the RequireHttpAttribute, serving 2 purposes:
/// 1) Allows debugging by ignoring the SSL requirement on localhost
/// 2) AppHarbor load balancing SSL corrector, see URL: http://support.appharbor.com/kb/tips-and-tricks/ssl-and-certificates
/// </summary>
[AttributeUsage(AttributeTargets .Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public sealed class RequireHttpsRemixAttribute : System.Web.Mvc.RequireHttpsAttribute
{
	#region " Public methods "

	public override void OnAuthorization(System.Web.Mvc.AuthorizationContext filterContext)
	{
		if (filterContext == null)
		{
			throw new ArgumentNullException ("filterContext");
		}
		else
		{
			if (filterContext.HttpContext.Request.IsSecureConnection)
			{
				//Connection is direct SSL                      
				return;
			}
			else if (string.Equals(filterContext.HttpContext.Request.Headers["X-Forwarded-Proto"], Uri.UriSchemeHttps, StringComparison .InvariantCultureIgnoreCase))
			{
				//Connection is SSL beyond the load balancer level
				return;
			}
			else
			{
				HandleNonHttpsRequest(filterContext);
			}
		}
	}

	#endregion

	#region " Protected methods "

	/// <summary>
	/// Could use the outer "If" statement to factor in SSL ignoring on specific testbed environments (ie. by specifying server names)
	/// </summary>
	protected override void HandleNonHttpsRequest(System.Web.Mvc.AuthorizationContext filterContext)
	{
		if (!filterContext.HttpContext.Request.Url.Host.Contains("localhost"))
		{
			if (!string.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.InvariantCultureIgnoreCase))
			{
				throw new InvalidOperationException("The requested resource can only be accessed via SSL");
			}
			else
			{
				string url = string.Format("{0}://{1}{2}", Uri.UriSchemeHttps, filterContext.HttpContext.Request.Url.Host, filterContext.HttpContext.Request.RawUrl);
				filterContext.Result = new System.Web.Mvc.RedirectResult (url);
			}
		}
	}

	#endregion
}

You’ll notice, if you read through the AppHarbor help page to which I’ve linked in the class’s comments, that this code is based on the AppHarbor team’s recommended implementation of the custom attribute. I have, however, spiced up their example with a few changes of my own, mainly the splitting of HTTPS and non-HTTPS checking into separate functions. The HandleNonHttpsRequest function allows you to run the same code on your localhost dev environment (which probably won’t have SSL, and thus would fail the previous checks) and also specify additional testbed server names (should you need that layer of staging before pushing the code to AppHarbor).

And there you have it; short and sweet.