require 'gem_plugin' require 'mongrel' require 'digest/sha1' # = Mongrel Secure Download Handler # # The need to send secured files in a fast and reliable way is common. # # Sending a file from inside of a web application can be slow # and also utilizes an entire application thread/process until the user # is done downloading the file. For large files this is inefficient. # The other option is to have the web server itself send the files as normal # static content. This is faster but means that the files have to be in a web # accessible public directory so that if someone guessed the URI of a file # they could gain access to it. This is not a reasonable solution for # situations where files need to be secured against unauthorized downloading. # # This handler addresses the problem of having a fast and secure # download mechanism for web applications. The mechanism works by having # the application generate a special URI containing a token that is only # valid for a certain period of time. The server then recognizes this URI # and generates a token using the parameters passed in and checks for a match # before sending the file to the user. The key to the process is the secret # string that both the server and the application are aware of. # # == URI Format # # A properly formed URI will have a path and query of the following format # # /?token=&relative-path=×tamp= # # === Where # # is a directory that does not exist in the directory structure of the application but does # exist in the directory structure of the server. # # example uri-prefix # # /downloads # # is the path to the file being requested, relative to the path # at which the web server is running. The web server must have permissions to read the file. # # example relative-path # # /files/your_secured_file.txt # # is the number of seconds since epoch until the time when this download expires # # example timestamp (ruby on rails) # # 1.minute.from_now.to_i # # is the SHA1 hash of the concatenation of the following items: # 1. the user defined secret string # 2. the relative path to the file # 3. the timestamp # # == Using the Handler # # To use the handler you need to do the following: # # Setup the handler within a configuration script and pass in the secret string. # # example configuration script # # uri "/downloads", :handler => plugin('/handlers/securedownload',{:secret_string => "my_secret_string"}) # # In your application form a secured URI by creating the proper parameters and # perform an SHA1 hash of the parameters to create the proper token # # example code (ruby on rails) # # require 'digest/sha1' # # secret_string = 'my_secret_string' # uri_prefix = '/downloads' # relative_path = '/files/secret_document.pdf' # timestamp = 1.minute.from_now.to_i # token = Digest::SHA1.hexdigest(secret_string + relative_path + timestamp) # uri = "#{uri_prefix}/?token=#{token}&relative-path=#{relative_path}×tamp=#{timestamp}" # # Start mongel by passing in the location of the configuration script from step 1 with the -S command # line switch. # # example mongrel start command # # mongrel_rails start -S config/secure_download_config.rb # # == Error messages # # If any of the parameters in the URI or the secret_string are missing # the handler returns a 500 Application Error. # # If the token passed in as a parameter does not match the token generated # by the handler (if someone tries to guess the token) the handler returns # a 403 Forbidden error. # # If the timestamp is earlier than the current server time, meaning that the file is # no longer a valid download then the handler returns a 408 Request Time-out Error. # This error is not technically correct but it makes the most sense in the context of # the handler. class SecureDownload < GemPlugin::Plugin "/handlers" include Mongrel::HttpHandlerPlugin def process(request, response) query = Mongrel::HttpRequest.query_parse(request.params['QUERY_STRING']) if @options[:secret_string].nil? or query['token'].nil? or query['timestamp'].nil? or query['relative-path'].nil? response.start(500){} elsif query['timestamp'].to_i < Time.now.to_i response.start(408){} elsif query['token'] == Digest::SHA1.hexdigest("#{@options[:secret_string]}#{query['relative-path']}#{query['timestamp']}").to_s send_file(File.expand_path("." + query['relative-path']), response) else response.start(403){} end end private # Sends the contents of a file back to the user. def send_file(path, response) # first we setup the headers and status then we do a very fast send on the socket directly file_status = File.stat(path) response.status = 200 # Set the last modified times as well and etag for all files response.header[Mongrel::Const::LAST_MODIFIED] = file_status.mtime.httpdate # Calculated the same as apache, not sure how well the works on win32 response.header[Mongrel::Const::ETAG] = Mongrel::Const::ETAG_FORMAT % [file_status.mtime.to_i, file_status.size, file_status.ino] #set the content type to something generic for now response.header[Mongrel::Const::CONTENT_TYPE] = @default_content_type #set the content disposition and filename response.header['Content-Disposition'] = "attachment; filename=\"#{File.basename(path)}\"" # send a status with out content length response.send_status(file_status.size) response.send_header if not header_only response.send_file(path) else response.send_body # should send nothing end end end