Tuesday, January 25, 2011

Workaround an IE bug: Redirecting subdomains to www while preserving virtual hosts (Nginx, Rails)

I ran into an interesting redirect configuration problem that was exacerbated by Internet Explorer's handing of cookies, which led me to move all subdomain redirection logic out of my Rails app and into Nginx. In the end, this feels like a good thing anyway. If you just want the conclusion, skip to the solution section.

Background



My original setup was to have nginx have virtual hosts for www.example.com, test.example.com, and staging.example.com, where the www.example.com was the default one that would catch anything else such as foo.example.com, or just example.com. Then the Rails app at www.example.com would check the URI in a before_filter and redirect to www.example.com, using a solution similar to the one described here: http://stackoverflow.com/questions/327122/redirect-myapp-com-to-www-myapp-com-in-rails-without-using-htaccess.

Problem


The problem arose when an Internet Explorer user hit the site via http://example.com (i.e.: without the "www." prefix). Here is what happens when someone hits example.com in this setup:
  1. The default virtual host for www.example.com picks up the request and hands it to the Rails app.
  2. The Rails app's before_filter sees that the request.host does not start with 'www', and does a redirect.
  3. Somewhere the ActionController internals set a session cookie on the domain "example.com" -- even though this before_filter redirects BEFORE any code tries to touch the session. (Perhaps this is an artifact of using CookieStore.)
  4. The browser stores the session cookie for example.com, the follows the redirect to www.example.com, sending that cookie (which is basically an empty session at this point).
  5. The user logs in. The Rails app sets stuff in the session and returns a response that sets the new session cookie with the new session, for www.example.com.
  6. The browser now has two session cookies: One not-logged-in one for example.com, and one logged-in one for www.example.com.
  7. The next page load is where the problems start...
On Safari, Chrome, and Firefox, the next page load, on www.example.com, only sends the session cookie that is for www.example.com...and all is well. However, on Internet Explorer (tested on IE8 and IE9 beta), the browser sends BOTH session cookies, which both have the same name. In my observation, Rails always choose the first one, which is always the LEAST specific one - the one for example.com. Now, even though the user just logged in, he is not logged in.

I don't know what the HTTP RFC for user agents says about this, but I am just gonna go ahead and call this in IE bug. According to an MSDN FAQ about IE's cookie-handling, IE is different than other browsers, and they're just fine with that (see Q3 on that page). Also see Q1 where they say they don't even try to support the RFC for cookies.

Similar problems have been documented on the web. I read somewhere that if your Rails app redirects before touching the session, that you won't have this problem. That's not the case for me, but I suspect that may have to do with using CookieStore. I can imagine that CookieStore touches the session by reading it when it comes time to generate the cookie, and that this gets triggered even after a redirect.

Redirect with Nginx


Since I could not get Rails to NOT set a cookie for example.com, I decided I would do this redirect in nginx. There are plenty of examples on the web about redirecting example.com to www.example.com, or vice-versa. However, most of them don't work for my needs for one particular reason: virtual hosts.

In my scenario, I can't redirect ALL requests to non-www subdomains to the www-subdomain, because I have other virtual hosts on specific sub-domains that should not be re-routed. However, I can't simply enumerate the ones that I want redirected, since that is meant to be a catch-all. Rather than try to maintain a regular expression that matches all subdomains except the ones I setup virtual hosts on, I decided to just test the hostname inside the default virtual host, as shown in the conclusion section below. This way, specific subdomains get routed to the right virtual host, anything else gets routed to the www.example.com virtual host, and within that one, anything that isn't www.example.com gets redirected to www.example.com.

Solution


The final solution: this nginx config mockup shows how I can have specific subdomains handled by their virtual hosts, and everything else redirected to the www subdomain.



Using tcpdump to sniff HTTP traffic from a specific host

This is mostly just a reminder to myself about my preferred parameters to tcpdump on linux, so that I don't have to keep reading the man page.

tcpdump -c 20 -s 0 -i eth1 -A host 192.168.1.1 and tcp port http

The parameter breakdown:
  • -c 20: Exit after capturing 20 packets.
  • -s 0: Don't limit the amount of payload data that is printed out. Print it all.
  • -i eth1: Capture packets on interface eth1
  • -A: Print packets in ASCII.
  • host 192.168.1.1: Only capture packets coming to or from 192.168.1.1.
  • and tcp port http: Only capture HTTP packets.

Friday, December 10, 2010

Hate the emacs tilde backup files?

I do...but for decades I have been afraid to turn them off in case I ever needed the backup file, even though I have never used them once. Finally I snapped and tried to figure out how to turn them off. While looking this up, I came across a better option: store them all in a single hidden directory, instead of littering the whole filesystem with them. There is a nice package called "backup-dir" that even provides the ability to keep multiple versions of your backup files, while keeping them all out of sight and out of mind, and automatically cleaning up old versions. Learn more at:


To see how I pulled in the backup-dir package and set it up in my init.el file, see the change to my dotfiles project.