How to serve protected downloads with Rails

24 Nov 2006

Do you have a site where you sell downloads? Perhaps you sell e-books, screencasts, or MP3s. Would you like a way to protect these downloads by using your Rails application to authenticate users before they can download the file, but you don’t want to tie up a Rails process to actually serve the file? Lighttpd’s mod_secdownload was built specifically for this situation, but there’s a little gotcha for those of you who are also using mod_proxy to pass traffic to mongrel…

First, here’s a quick summary of how to use mod_secdownload to offload file serving from your Rails application while still having your application verify that someone can access the files. Here’s the lighttpd configuration (don’t forget to load mod_secdownload via server.modules):

secdownload.secret = ‘asupersecretstring’ secdownload.document-root = ’/a/non-public/path’ secdownload.uri-prefix = ’/dl/’

And here’s some code for creating a link from your Rails application (from the lighttpd wiki):

def gen_sec_link(rel_path)
    rel_path = "/#{rel_path}" unless rel_path.starts_with?(’/’)
    s_secret = ‘asupersecretstring’             # Secret string
    uri_prefix = ’/dl/’             # Arbitrary download prefix
    timestamp = "%08x" % Time.now.to_i  # Timestamp, to hex
    token = MD5::md5(s_s + rel_path + timestamp).to_s    # Token Creation
    ’%s%s/%s%s’ % [uri_prefix, token, timestamp, rel_path]   # Return the properly formatted string
  end

The relative path here is relative from secdownload.document-root, so for the file /a/non-public/path/seo-secrets.pdf, rel_path would be /seo-secrets.pdf.

In my application I have a model associated with files that can be purchased, and I have an instance method on that class that generates the mod_secdownload link. My view links to a controller action that redirects the user to the generated link. This prevents a download link from expiring while the user is viewing the page. This view can only be accessed once the user has logged in, and the view contains links only to the files that have been purchased. Similarly, the controller uses scoping to ensure has purchased the file that he is attempting to download.

Now for the gotcha. You’ll notice from the configuration for the module that you specify a virtual root path that is used in generating the URLs. In my case, I chose ’/dl’. The problem comes when you are also using mod_proxy like so:

$HTTP["host"] =~ "examplehost\.com" {
    server.document-root = "/var/rails/exampleapp/current/public/"
    server.indexfiles = ( "index.html", "dispatch.fcgi" )
    proxy.balance = "fair"
    $HTTP["url"] !~ "/(javascripts|stylesheets|images)" {
      proxy.server  = ( "/" => (
        ( "host" => "127.0.0.1", "port" => 3000 )
      ) )
    }
  }

This is a typical lighttpd configuration for serving a Rails app from mongrel. The key part to notice is the proxy configuation, which passes all requests on to mongrel except for those for the javascripts, stylesheets, and images paths in the public path. The problem with this configuration is that traffic destined for the fake ’/dl’ path also gets sent to mongrel, which passes it on to the Rails application, which has no idea how to handle it (unless you happen to have a ‘dl’ controller). So, all you have to do is add ‘dl’ to the list of excluded URLs in the proxy configuration:

$HTTP["url"] !~ "/(dl|javascripts|stylesheets|images)" {

Now requests for ’/dl/...’ will get skipped by the proxy and consumed by mod_secdownload.

This is an excellent way to serve up content without typing up your Rails application and without just giving it away.


Actions

Informations

4 responses to “How to serve protected downloads with Rails”

Tim Lucas (05:06:02) :

with the next version of Lightty you can use the “x-send-file” header to specify a file from disk to be served up:
http://blog.lighttpd.net/articles/2006/07/22/mod_proxy_core-got-x-sendfile-support

and with nginx you can specify “x-accel-redirect” header to accomplish the same thing:
http://blog.kovyrin.net/2006/11/01/nginx-x-accel-redirect-php-rails/

I’ve got mod_secdownload running on one project but I’m definitely thinking about switching to this as it makes things much simpler.

Jeremy McAnally (10:36:35) :

Wouldn’t it be simpler to use send_file and an authenticated action? Move the file(s) out of your Rails app’s root and then use send_file to send them to the user. That’s how I do my eBooks at least. :)

Ben (12:24:57) :

@Tim: I’m looking forward to the next version of lighty because of conveniences like x-send-file. It’s definitely a simpler solution.

@Jeremy: It is simpler to use send_file, but the problem is that it ties up a Rails process. This isn’t a big deal if you are sending small files, but the bigger the files get the worse of a problem this becomes.

Elliot Smith (04:03:53) :

Another alternative would be to put the downloadables onto Amazon S3 under time-expiring URLs with hashed signatures. Although in that situation you would have to pay for the bandwidth through S3, of course.