A file download gateway. This is particularly useful for keeping a private file repository out of the web server’s document root and governing access per user. Assumes you are using Action Pack 0.8.0 (Rails 0.6.5) or later…
app/views/file/index.rhtml
View a list of downloads:
<p style="color: red;"><%=h @flash['error'] %></p>
<ul><strong>Permitted</strong>
<% for file in @good %>
<li><%= link_download file %></li>
<% end %>
</ul>
<ul><strong>Denied</strong>
<% for file in @bad %>
<li><%= link_download file %></li>
<% end %>
</ul>
app/helpers/file_helper.rb
Helper to create download links:
module FileHelper
def self.append_features(controller) #:nodoc:
if controller.ancestors.include? ActionController::Base
controller.add_template_helper self
else
super
end
end
def link_download(file)
link_to CGI.escapeHTML(file), :action => 'download',
:params => { 'file' => file }
end
end
app/controllers/file_controller.rb
Controller acts as a file download gateway. Respond with 304 Not Modified unless the file has been modified since the If-Modified-Since date in the request header. Possible extension: resumable downloads by responding to the Range request header with 206 Partial Content status and Content-Range header.
In the below code, what is ‘abstract_application’? Also, this doesn’t seem to work – trying to add the MissingFile class raises an exception about an undefined defined constant ActionControllerError.
With Rails 0.9.0, the Abstract Application Controller was renamed ApplicationController, which means that you must require ‘application’ rather than ‘abstract_application’. To avoid the undefined constant errors, move the class declaration outside of the FileController class declaration, which allows the MissingFile class to see the ActionController classes.
Additionally, to protect your files from direct access, but still allow them to be downloaded, create a directory for your files outside of the web space (a /files/ directory on the same level as the /public/ directory and path your files from there. Next, using RAILS_ROOT as the root, change the bath_path method to return your file directory path (e.g. RAILS_ROOT + “/files”) and when you get the path (by calling the sanitize_file_path, pass the filename, and then base_path +
require 'application'
#require 'abstract_application'
require 'file_helper'
class MissingFile < ActionController::ActionControllerError #:nodoc:
end
#class FileController < AbstractApplicationController
class FileController < ApplicationController
include FileHelper
#class MissingFile < ActionController::ActionControllerError #:nodoc:
#end
protected
def base_path
File.dirname(__FILE__)
end
def permit_file?(path)
true
#@session['user'] and @session['user'].permit_file?(path)
end
public
def index
to_root = '../' * File.dirname(__FILE__).count(File::SEPARATOR)
@good = [ File.basename(__FILE__) ]
@bad = [
'../<< "&',
'/tmp/mysql.sock',
'/etc/passwd',
"#{to_root}etc/passwd",
'`cat /etc/passwd`',
'../../config/database.yml',
]
end
def download
begin
path = sanitize_file_path(@params['file'], base_path)
raise MissingFile, 'permission denied' unless permit_file? path
if http_if_modified_since? path
send_file path
else
render_text '', '304 Not Modified'
end
rescue MissingFile => e
flash['error'] = "Download error: #{e}"
redirect_to :action => 'index'
end
end
protected
# Safely resolve an absolute file path given a malicious filename.
def sanitize_file_path(filename, base_path)
# Resolve absolute path.
path = File.expand_path("#{base_path}/#{filename}")
logger.info("Resolving file download: #{filename}\n => #{base_path}/#{filename}\n => #{path}") unless logger.nil?
# Deny ./../etc/passwd and friends.
# File must exist, be readable, and not be a directory, pipe, etc.
raise MissingFile, "couldn't read #{filename}" unless
path =~ /^#{File.expand_path(base_path)}/ and
File.readable?(path) and
File.file?(path)
return path
end
# Check whether the file has been modified since the date provided
# in the If-Modified-Since request header.
def http_if_modified_since?(path)
if since = @request.env['HTTP_IF_MODIFIED_SINCE']
begin
require 'time'
since = Time.httpdate(since) rescue Time.parse(since)
return since < File.mtime(path)
rescue Exception
end
end
return true
end
end
category:Howto
Additional Material
*Quick rundown of how to use send_file and a few concerns regarding it
A file download gateway. This is particularly useful for keeping a private file repository out of the web server’s document root and governing access per user. Assumes you are using Action Pack 0.8.0 (Rails 0.6.5) or later…
app/views/file/index.rhtml
View a list of downloads:
<p style="color: red;"><%=h @flash['error'] %></p>
<ul><strong>Permitted</strong>
<% for file in @good %>
<li><%= link_download file %></li>
<% end %>
</ul>
<ul><strong>Denied</strong>
<% for file in @bad %>
<li><%= link_download file %></li>
<% end %>
</ul>
app/helpers/file_helper.rb
Helper to create download links:
module FileHelper
def self.append_features(controller) #:nodoc:
if controller.ancestors.include? ActionController::Base
controller.add_template_helper self
else
super
end
end
def link_download(file)
link_to CGI.escapeHTML(file), :action => 'download',
:params => { 'file' => file }
end
end
app/controllers/file_controller.rb
Controller acts as a file download gateway. Respond with 304 Not Modified unless the file has been modified since the If-Modified-Since date in the request header. Possible extension: resumable downloads by responding to the Range request header with 206 Partial Content status and Content-Range header.
In the below code, what is ‘abstract_application’? Also, this doesn’t seem to work – trying to add the MissingFile class raises an exception about an undefined defined constant ActionControllerError.
With Rails 0.9.0, the Abstract Application Controller was renamed ApplicationController, which means that you must require ‘application’ rather than ‘abstract_application’. To avoid the undefined constant errors, move the class declaration outside of the FileController class declaration, which allows the MissingFile class to see the ActionController classes.
Additionally, to protect your files from direct access, but still allow them to be downloaded, create a directory for your files outside of the web space (a /files/ directory on the same level as the /public/ directory and path your files from there. Next, using RAILS_ROOT as the root, change the bath_path method to return your file directory path (e.g. RAILS_ROOT + “/files”) and when you get the path (by calling the sanitize_file_path, pass the filename, and then base_path +
require 'application'
#require 'abstract_application'
require 'file_helper'
class MissingFile < ActionController::ActionControllerError #:nodoc:
end
#class FileController < AbstractApplicationController
class FileController < ApplicationController
include FileHelper
#class MissingFile < ActionController::ActionControllerError #:nodoc:
#end
protected
def base_path
File.dirname(__FILE__)
end
def permit_file?(path)
true
#@session['user'] and @session['user'].permit_file?(path)
end
public
def index
to_root = '../' * File.dirname(__FILE__).count(File::SEPARATOR)
@good = [ File.basename(__FILE__) ]
@bad = [
'../<< "&',
'/tmp/mysql.sock',
'/etc/passwd',
"#{to_root}etc/passwd",
'`cat /etc/passwd`',
'../../config/database.yml',
]
end
def download
begin
path = sanitize_file_path(@params['file'], base_path)
raise MissingFile, 'permission denied' unless permit_file? path
if http_if_modified_since? path
send_file path
else
render_text '', '304 Not Modified'
end
rescue MissingFile => e
flash['error'] = "Download error: #{e}"
redirect_to :action => 'index'
end
end
protected
# Safely resolve an absolute file path given a malicious filename.
def sanitize_file_path(filename, base_path)
# Resolve absolute path.
path = File.expand_path("#{base_path}/#{filename}")
logger.info("Resolving file download: #{filename}\n => #{base_path}/#{filename}\n => #{path}") unless logger.nil?
# Deny ./../etc/passwd and friends.
# File must exist, be readable, and not be a directory, pipe, etc.
raise MissingFile, "couldn't read #{filename}" unless
path =~ /^#{File.expand_path(base_path)}/ and
File.readable?(path) and
File.file?(path)
return path
end
# Check whether the file has been modified since the date provided
# in the If-Modified-Since request header.
def http_if_modified_since?(path)
if since = @request.env['HTTP_IF_MODIFIED_SINCE']
begin
require 'time'
since = Time.httpdate(since) rescue Time.parse(since)
return since < File.mtime(path)
rescue Exception
end
end
return true
end
end
category:Howto
Additional Material
*Quick rundown of how to use send_file and a few concerns regarding it