This is the second post in a two part series (part 1 is here) on adding encryption to attachment_fu for Rails applications.

What about making the file available for download? AVOIDING THE ISSUE OF SCALABILITY FOR A MOMENT (since sendfile is not the right way to serve files from Rails), we want to use a variant of sendfile to do the decryption and send the file. Here’s a modified version of send_file that uses an extra hash parameter (acme) to decrypt if provided:

module ActionController
   module Streaming

   def send_file_x(path, options = {}) #:doc:
      raise MissingFile, "Cannot read file #{path}" unless File.file?(path) and File.readable?(path)

      options[:length]   ||= File.size(path)
      options[:filename] ||= File.basename(path)
      send_file_headers! options

      @performed_render = false
      logger.warn("Sending file #{path}")
      if options[:stream]
        render :status => options[:status], :text => Proc.new { |response, output|
          logger.info "Streaming file #{path}" unless logger.nil?
          len = options[:buffer_size] || 4096
          if options[:acme]
            c = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
            c.decrypt
            c.key = key = options[:acme]
            c.iv = iv = Digest::SHA1.hexdigest("OneFishTwoFish")
          end
          File.open(path, 'rb') do |file|
            while buf = file.read(len)
              if options[:acme]
                output.write(c.update(buf))
              else
                output.write(buf)
              end
            end
          end
        }
      else
        logger.info "Sending file #{path}" unless logger.nil?
        File.open(path, 'rb') { |file| render :status => options[:status], :text => file.read }
      end
    end
  end
end

The code could be made more efficient by not performing the options[:acme] test each time a buffer is written. Our controller action that downloads a file would call it like so:

send_file_x(@file_item.stored_filename,
  :filename      =>  @file_item.filename,
  :type             =>  @file_item.content_type,
  :disposition  =>  'attachment',
  :stream    =>  'true',
  :buffer_size  =>  4096,
  :acme => @file_item.acme)

In a production environment, send_file consumes to many server resources – the rails application, and method used to service it (FastCGI, Mongrel, etc.) are tied up serving the file.

It’s more likely the case that the rails application will be behind a reverse proxy like nginx; in that case, a directive is sent to the server to provide the file (usually through an HTTP header). For nginx, serving a non-encrypted, static file would be done by sending a header with the location of a file:

if defined?(NGINX_FOR_DOWNLOAD) && NGINX_FOR_DOWNLOAD
  # code omitted – set up file name and path
  response.headers['X-Accel-Redirect'] = NGINX_PATH_FOR_ _DOWNLOAD + path
  response.headers['Content-Type'] = file_item.content_type
  render :nothing=>true;
else
  send_file_x(File.join(RAILS_ROOT, FILE_STORAGE_PATH, path_parts, file_item.filename),
      :type         => file_item.content_type,
      :disposition  => 'attachment',
      :stream    => 'true',
      :buffer_size  => 4096,
      :acme      => nil,
      :encoding     => 'utf8',
      :filename     => URI.encode(file_item.filename))
End

For more information on nginx and rails, learn more about NginxXSendfile.

To perform a similar feat of decrypting and sending a file for nginx, a new module would need to be written for nginx that takes an additional header variable ‘X-Accel-Redirect-Key’ and uses that as the key to send the file, decrypting as it goes.

This website uses IntenseDebate comments, but they are not currently loaded because either your browser doesn't support JavaScript, or they didn't load fast enough.

Leave a Reply