require 'digest/sha1' require 'open-uri' require 'rubygems' require 'mongrel' require 'filemagic/ext' class SecureDownloadRedux < GemPlugin::Plugin '/handlers' include Mongrel::HttpHandlerPlugin URL_RE = %r{\A(?:ht|f)tps?://}io attr_reader :response, :secret, :base, :path, :timestamp, :token def process(request, response) query = Mongrel::HttpRequest.query_parse(request.params['QUERY_STRING']) @response = response @secret = @options[:secret] @base = @options[:base] || '.' @path = query['path'] @timestamp = query['timestamp'] @token = query['token'] if !required_params_given? || timeout? || !authorized? response.start(@status) {} else @status = 200 # OK url? ? send_url : send_file end end private def required_params_given? @status = 500 # Internal Server Error secret && path && timestamp && 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 # Choose your alternative: alias_method :send_url, :send_url_redirect2 def send_file path = File.expand_path(File.join(base, @path)) # Prevent double-dot vulnerability! return unless path =~ %r{\A#{Regexp.escape(File.expand_path(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