Showing posts with label http. Show all posts
Showing posts with label http. Show all posts

Tuesday, August 16, 2011

Rails incorrectly parses HTTP Accept header for single media with range

Try hitting your Rails app with an HTTP header like this:

Accept: text/html;q=0.7



You will most likely get an exception that looks something like this:

Missing template home/index with {:locale=>[:en, :en],  :formats=>["text/html;q=0.7"], :handlers=>[:rhtml, :erb, :haml,  :rxml, :builder, :rjs]} in view paths  "/path/to/my_app/app/views"



This is because the ActionPack Mime::Type class is buggy in the way it parses this header when there is only one media type specified, and that media type has a range specified (the ";q=0.7"). This is perfectly valid HTML, and ActionPack actually handles the ranges correctly when there are more than one type specified.

An issue has been open for a while on this: https://github.com/rails/rails/issues/736 -- working patches have been proposed but not applied, yet the issue got closed for some reason.

I prefer not to patch my own version of Rails, so instead I am using a small monkey patch that works around this problem. I've created a file named config/initializers/rails_issue_736.rb that fixes the Mime::Type.lookup method to only use the part that comes before the semi-colon:


Tuesday, May 31, 2011

Encrypted Cookie Store that works with Rails 3.0

If you like the benefits using Rails's CookieStore, but want to store possibly sensitive data in the cookie, then encrypted_cookie_store is for you. It works like CookieStore, but encrypts the cookie payload so that it cannot be read by the client.

If you are using Ruby on Rails 3.0.0 or higher (but not yet Rails 3.1), then you'll need my fork of encrypted_cookie_store that fixes it for Rails 3.

To install it:

gem install scottwb-encrypted_cookie_store

Then add to your bundler Gemfile:

gem 'scottwb-encrypted_cookie_store', :require => 'encrypted_cookie_store'

Then read the rest of the installation/configuration instructions.

Background

For Rails 2.3, the folks at Phusion created encrypted_cookie_store, which you'd install as a plugin, and it worked great. I used it for a long time.

However, they never updated it for Rails 3. That's where Ben Sales came in. He forked this project and made it work for Rails 3...in the pre-release days, that is. Ben did all the work to get it packaged up as a gem and updated it to work with Rails 3 railties and initializers.

Unfortunately, sometime between Rails 3.0.0.beta3 and 3.0.0.beta4, the layout of AbstractStore and CookieStore changed quite a bit, pushing a lot of the functionality out to Rack, and breaking the encrypted_cookie_store gem.

That's where I come in. I basically did the minimal amount of work required to get it to work with Rails 3.0 (tested on 3.0.0, 3.0.7, and 3.0.8.rc4), got all the specs working again, and created a new gem called 'scottwb-encrypted_session_cookie'.

It doesn't work in Rails 3.1, but I'll probably remedy that once Rails 3.1 officially releases. I'm also happy to accept patches if anyone else onces to tackle that.

This is a nice gem. Maybe some day I'll make a push to clean it up and lobby to have it as one of the packaged options that ships with Rails...

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.