Fun and games

The application I’ve been working on this month uses FancyUpload to provide a snazzy UI for uploading image files that are then processed and stored using Paperclip and ImageMagick. FancyUpload is a JavaScript and Shockwave Flash based solution that gives you all kinds of goodness like multiple file uploads, progress bars and status information. Unfortunately getting FancyUpload, as well as any other form of flash based uploader such as SWFUpload, to play nice with Rails requires some work.

I won’t delve too deeply into the various problems as much has already been written about them, along with possible solutions. Instead here’s some background reading:

Unfortunately the CGI patching approach suggested in the above blogs presented a problem for me: I’m running on edge Rails which over the last couple of weeks has pretty much shed all of its CGI baggage so it’s unlikely the patches would work. What to do?

Bring on the wall middleware!

My first thought * was “surely this is something middleware can help out with” and so a quick Google yielded exactly that, the only problem being it was for Merb.

* actually my first thought was “it must be time for a tea break”, but middleware was a close second.

Here’s the code (by Angel Pizarro):

module Merb
  module Rack
    class SwfSetSessionCookie < Merb::Rack::Middleware
      # :api: private
      def initialize(app, session_key = '_session_id')
        super(app)
        @session_key = session_key
      end
      # :api: plugin
      def call(env)
        if env["HTTP_USER_AGENT"] =~ /Adobe Flash/
          prms = Merb::Parse.query(env['QUERY_STRING'])
          env['HTTP_COOKIE'] = [@session_key,prms[@session_key]].join('=').freeze
        end
        @app.call(env)
      end
    end
  end
end

What this does is check the request environment for Flash in the HTTP_USER_AGENT header, if a match is found then the request query string is parsed for session data (the session_key is passed to the middleware when it is initialised) which is then forced into the HTTP_COOKIE header so that when the request reaches Merb it treats the session data just like it would in any other request.

I’m pleased to say that converting this into middleware that will work with Rails (and in theory any other Rack application) was extremely simple, especially once I discovered that the Rack::Utils library provides an implementation of the Merb::Parse.query method. The only functional change I needed to make was to the HTTP_USER_AGENT regular expression so that it would work with newer versions of Flash. Here’s the code:

require 'rack/utils'

class FlashSessionCookieMiddleware
  def initialize(app, session_key = '_session_id')
    @app = app
    @session_key = session_key
  end

  def call(env)
    if env['HTTP_USER_AGENT'] =~ /^(Adobe|Shockwave) Flash/
      params = ::Rack::Utils.parse_query(env['QUERY_STRING'])
      env['HTTP_COOKIE'] = [ @session_key, params[@session_key] ].join('=').freeze unless params[@session_key].nil?
    end
    @app.call(env)
  end
end

Integration with Rails

Getting the middleware to work with Rails involved putting the above code in my newly created RAILS_ROOT/app/middleware folder in a file snappily named flash_session_cookie_middleware.rb. At the moment there doesn’t appear to be a convention for where these files should live, but this makes sense to me as the new Metal endpoints will live in RAILS_ROOT/app/metal. The middleware folder then needs adding to the load path in environment.rb:

config.load_paths += %W( #{RAILS_ROOT}/app/middleware )

Rails provides a new config.middleware.use setting that can be used in environment.rb to specify middleware classes, however because I wanted to pass the session key to the constructor I decided to do the following in my RAILS_ROOT/config/initializers/session_store.rb initializer instead (session store configuration was moved to an initializer in this commit):

ActionController::Base.session = {
  :session_key => '_my_app_session',
  :secret      => '--blah--' # Real key removed to protect the innocent
}

ActionController::Dispatcher.middleware.use FlashSessionCookieMiddleware, ActionController::Base.session_options[:session_key]

The first part of the initializer sets the session key and secret as normal, the next part calls ActionController::Dispatcher.middleware.use to register the FlashSessionCookieMiddleware class passing it the session key as a parameter.

The final step is to pass the session data and authenticity token as URL parameters so that they can be picked up by the middleware. In our application we have an assets controller, so our upload form_for originally looked like this:

form_for(@asset, :multipart => true)

This was changed like so:

form_for(@asset, :url => new_asset_path_with_session_information, :multipart => true)

And in our AssetsHelper module, a new_asset_path_with_session_information helper was added:

def new_asset_path_with_session_information
  session_key = ActionController::Base.session_options[:session_key]
  new_asset_path(session_key => cookies[session_key], request_forgery_protection_token => form_authenticity_token)
end

Our Javascript code then uses the form URL when initialising the FancyUpload component, when a file is uploaded it will use a HTTP POST to a URL like this:

/assets?_my_app_session=BAh7CDoPc2Vzc2lvbl9pZCIlODViNmRjMWU1ODZiMGEwNmUxZGEzNzE0MzkyMGU3NzM6DHVzZXJfaWRpBjoQX2NzcmZfdG9rZW4iMWhWWXJwWU1QeHZyWTVEdG42WVlJeEhEN21GNTdhVVFPUnd3dHJpNGNJYU09--87e9b5a0f7ac3050b0ee61e3a46ae08ce825319d&authenticity_token=hVYrpYMPxvrY5Dtn6YYIxHD7mF57aUQORwwtri4cIaM%3D

The middleware will take the data in the _my_app_session parameter and convert it back into a cookie, and Rails will use the authenticity_token parameter to verify the request. So as far as Rails is concerned this is a regular POST request using the current session with a valid authenticity token: it has no clue that this really came from the FlashUpload component.

This is just the beginning

This has been my first experiment in writing Rack middleware and I think it is a great example of how it can be used to solve problems in a clean, straight-forward way without having to resort to the hackery of days gone by. My middleware shows some really very simple functionality, however this should be enough to make you want to find out what more can be achieved. A good place to start is Ryan Tomayko’s rack-contrib repository on GitHub and the Rack development Google group.

Have fun, and try not to get too carried away!

An update for Rails edge: March 2009

Thanks to Saimon Moore for his comment pointing out that Rails edge has renamed the :session_key option to :key. If you’re running on edge then you’ll need to make the following changes:

  1. In the RAILS_ROOT/config/initializers/session_store.rb initializer:

    ActionController::Base.session = {
      :key     => '_my_app_session',
      :secret  => '--blah--' # Real key removed to protect the innocent
    }
    
    ActionController::Dispatcher.middleware.use FlashSessionCookieMiddleware, ActionController::Base.session_options[:key]
  2. And in the AssetsHelper#new_asset_path_with_session_information method:

    def new_asset_path_with_session_information
      session_key = ActionController::Base.session_options[:key]
      new_asset_path(session_key => cookies[session_key], request_forgery_protection_token => form_authenticity_token)
    end

An update for Rails 2.3.2: June 2009

Following his comment about the move of Rails cookie handling into middleware, David North kindly emailed us with the necessary changes to allow our Flash cookie to work with Rails 2.3.2. Without this change you will likely still get InvalidAuthenticityToken exceptions.

In the RAILS_ROOT/config/initializers/session_store.rb initializer we need to inject our middleware before the new Rails cookie middleware. To do this change the ActionController::Dispatcher.middleware.use call to this:

ActionController::Dispatcher.middleware.insert_before(ActionController::Session::CookieStore, FlashSessionCookieMiddleware, ActionController::Base.session_options[:key])

You can double check your middleware order is correct using the rake middleware task on the command line. The output should look something like this:

use Rack::Lock
use ActionController::Failsafe
use ActionController::Reloader
use FlashSessionCookieMiddleware, "_my_app_session"
use ActionController::Session::CookieStore, #<Proc:0x02445eb0@(eval):8>
use ActionController::RewindableInput
use ActionController::ParamsParser
use Rack::MethodOverride
use Rack::Head
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
run ActionController::Dispatcher.new

As you can see our FlashSessionCookieMiddleware appears before ActionController::Session::CookieStore which means our cookie fiddling will take place before Rails tries to use it.

If you want to find out more on Rails and Rack integration you should take a look at the new Rails on Rack guide.

A better way of inserting middleware: July 2009

Tung Nguyen commented with a better, more generic way of adding the middleware to your stack, meaning you no longer have to double check where to insert it:

ActionController::Dispatcher.middleware.insert_before(ActionController::Base.session_store, FlashSessionCookieMiddleware, ActionController::Base.session_options[:key])