require 'digest/sha1' require 'open-uri' require 'rubygems' require 'mongrel' require 'filemagic/ext' class SecureDownloadRedux < GemPlugin::Plugin '/handlers' # Our version ;-) VERSION = '0.0.4' include Mongrel::HttpHandlerPlugin URL_RE = %r{\A(?:ht|f)tps?://}io def initialize(options = {}) super @base = File.expand_path(@options[:base] || '.') if @base == '/' raise ArgumentError, 'specifying a base path of / is way too dangerous!' end @secret = @options[:secret] if @secret.nil? || @secret.empty? raise ArgumentError, 'secret missing' end @url_method = @options[:url_method] || 'redirect2' if respond_to?(method = "send_url_#{@url_method}", true) self.class.send(:alias_method, :send_url, method) else raise ArgumentError, "unknown URL method #{@url_method}" end end def process(request, response) @query = Mongrel::HttpRequest.query_parse(request.params['QUERY_STRING']) if !required_params_given? || timeout? || !authorized? response.start(@status) {} else @status = 200 # OK @response = response url? ? send_url : send_file end end private def required_params_given? @status = 500 # Internal Server Error @path = @query['path'] and @timestamp = @query['timestamp'] and @token = @query['token'] end def timeout? @status = 408 # Request Timeout @timestamp.to_i < Time.now.to_i end def authorized? @status = 403 # Forbidden @token == compute_token end def compute_token Digest::SHA1.hexdigest(@secret + @path + @timestamp) end def url? @path =~ URL_RE end def send_url_read @response.body = open(@path) unless @header_only @response.send_body end def send_url_redirect1 @status = 303 # See Other vs. Found (302) vs. Temporary Redirect (307) @response.start(@status, true) { |head, body| head['Location'] = @path #head['Content-type'] = ??? body.write(%Q{See #{@path}}) } end def send_url_redirect2 @response.socket.write(Mongrel::Const::REDIRECT % @path) end def send_file path = File.expand_path(File.join(@base, @path)) # Prevent double-dot vulnerability! return unless path =~ %r{\A#{Regexp.escape(@base)}/} file = File.stat(path) size = file.size time = file.mtime @response.status = @status @response.header[Mongrel::Const::LAST_MODIFIED] = time.httpdate @response.header[Mongrel::Const::ETAG] = Mongrel::Const::ETAG_FORMAT % [time.to_i, size, file.ino] @response.header[Mongrel::Const::CONTENT_TYPE] = File.content_type(path) || @default_content_type @response.header['Content-Disposition'] = %Q{inline; filename="#{File.basename(path)}"} @response.send_status(size) @response.send_header @header_only ? @response.send_body : @response.send_file(@path) end end