TL;DR
With a little JavaScript it’s possible to detect if a browser supports Server Name Indication and conditionally redirect to a HTTPS page. Code at the end of this post.
Trouble, thy name is Shared Hosting with SSL
Recently, I had to put a client into Amazon CloudFront for a high traffic event. I’ve done this a number of times, but this was the first time I need HTTPS support. Amazon offers HTTPS in two forms:
- All Clients (Dedicated IPs)
- Only Clients that Support Server Name Indication (SNI) “Older browsers and other clients that don’t support SNI can’t access your content over HTTPS.”
All clients sounds safe right? Well, it is, but it’s also very expensive and enabling it requires that you go through an approval process.
SNI is cheap and easy, but… I was aware of it’s existence, and knew it was a pain because of limited browser support. Time to check that assumption.
First, however, we need to understand the different types of hosting and, to do that, you need to know a little about how HTTP and SSL/TLS work.
How We Host
There are two (common) ways for a web server to serve a website.
IP-based Virtual Hosting. In this configuration each web site has it’s own IP address. When an HTTP request comes in server only cares about the request path, the part of the URL after the hostname.
At first, IP-based hosting was the only option. The HTTP request didn’t include the hostname part of the URL in the requested, just the path. However, because IP addresses are relatively scarce and expensive, an option to share them was needed. So, the HTTP 1.1 standard, which came into common use in 1996 (but wasn’t actually a standard until 1997), added the hostname to the request in the form of the “Host” HTTP header.
Having the hostname in the request allowed the development of Name-based Virtual Hosting. In this case a web server serves multiple sites using a single IP address. When an HTTP request comes in the server uses the hostname in the Host header to look up the site and then processes the path portion of the request.
Using Name-based Hosting one IP can serve dozens, if not hundreds, of web sites.
But…
It all falls apart when HTTPS comes into the mix.
HTTPS is an HTTP request traveling over a connection that has been encrypted with SSL/TLS. The problem is the over, the secure connection is established before the HTTP request is sent.
TLS Negotiation
When a web browser connects to a web server using HTTPS, there is a handshake process which includes download the sites SSL certificate to verify it’s identity. If the certificate doesn’t match the name the visitor entered/clicked on in the browser, an error is raised. This is designed to prevent Man in the Middle Attacks and other sorts of spoofing.
The works fine for one site with one name and one SSL certificate. However, it does not play nicely with name-based hosting. The hostname is sent in the HTTP request after the TLS handshake meaning the server can’t know what SSL certificate to use until the negotiation is completed.
Enter SNI
SNI solves the problem by sending the hostname as part of the TLS negotiation. The web server can then choose the correct certificate for the negotiation and Christmas is saved!
But…
Unfortunately, not all browsers support SNI. There’s IE6, of course, but also any version of IE running on Windows XP (because the underlying SSL library doesn’t support SNI). When running on Windows Vista or greater, IE7 or greater does support SNI. There are also a few, old, but still common in the wild, versions of the Android browser that also don’t support SNI.
The Work Around
The typical approach to dealing with SNI browser support is redirect browsers that support it to your HTTPS page and send browsers that don’t to some sort of snarky upgrade-your-browser page.
If you Google “SNI redirect” you’ll find solutions that either redirect visitors using a whitelist of browsers supporting SNI or a blacklist of those that don’t. There are a few problems with this kind of approach.
A whitelist has to be maintained, as new browsers come along they need to be added to the list. Otherwise, visitors on the latest, SNI loving browsers are going to receive your sub-par no SNI experience.
A blacklist could in theory be complete and not need to be updated. However, in practice this is easier said that done, and is complicated by the fact that Internet Explorer may or may not support SNI depending on the OS it’s running on.
Then there’s the fact that this approach redirects server side. If you are running in a CDN, like CloudFront, you want everything to happen browser side so traffic is not hitting the backend server.
A Better Way
Instead of trying to maintain a list of supported or unsupported browsers, what if we could simple detect SNI support? In a perfect world, the browser would report it’s capabilities, but they don’t. However, it is possible to test for SNI.
I can’t take credit for this idea. I found it in this post while wading through a sea of sample Apache redirect configurations.
The concept is simple, if a browser that doesn’t support SNI tries to
load SNI content, it will get an error. If we can test this in the
background and differentiate between error and success, then we can
redirect the visitor accordingly. The key to this turns out to be an
img tag. An img tag has an onload
callback, fired when the image is
successfully loaded and a, rarely used, onerror
callback, fired if
there is an error fetching the image
Armed with idea, we create a function that adds a hidden image to the page uses then callbacks to send the visitor to the correct page.
function secure_redirect() {
var img = document.createElement("img"); // create an img element.
// Set the src to an SNI URL of a one pixel image
img.src = "https://www.example.org/pixel.gif";
// This executes if SNI works.
img.onload = function () {
// Redirect to the secure page.
window.location.href = "https://example.org/";
};
// This executes if SNI doesn't work.
img.onerror = function (e) {
// Redirect elsewhere
window.location.href = "http://example.org/snarky-old-browser-message";
};
// Don't actually display the image
img.style.display = "none";
// but append it to the pages so it gets loaded.
document.body.appendChild(img);
}
And really, it’s even simpler
Not the code, but the concept. A clever reader might notice that the code isn’t actually testing for SNI, just the ability to securely load the image. If the HTTPS URL in question happens not require SNI, there’s only one cert or the first cert matches the requested domain, it still works. The problem has been reduced to “Can this visitor’s browser display the secure site or not?” and at the end of the day, that’s all that actually care about.
The moral of the story? “Test don’t guess.”
Lock image some rights reserved by Marcelo César Augusto Romeo.
Comments