Mongrel::DirHandler

Serves the contents of a directory. You give it the path to the root where the files are located, and it tries to find the files based on the PATH_INFO inside the directory. If the requested path is a directory then it returns a simple directory listing.

It does a simple protection against going outside it’s root path by converting all paths to an absolute expanded path, and then making sure that the final expanded path includes the root path. If it doesn’t than it simply gives a 404.

If you pass nil as the root path, it will not check any locations or expand any paths. This lets you serve files from multiple drives on win32. It should probably not be used in a public-facing way without additional checks.

The default content type is “text/plain; charset=ISO-8859-1” but you can change it anything you want using the DirHandler.default_content_type attribute.

Attributes

default_content_type[RW]
path[R]

Public Class Methods

add_mime_type(extension, type) click to toggle source

There is a small number of default mime types for extensions, but this lets you add any others you’ll need when serving content.

# File lib/mongrel/handlers.rb, line 276
def DirHandler::add_mime_type(extension, type)
  MIME_TYPES[extension] = type
end
new(path, listing_allowed=true, index_html="index.html") click to toggle source

You give it the path to the directory root and and optional listing_allowed and index_html

# File lib/mongrel/handlers.rb, line 121
def initialize(path, listing_allowed=true, index_html="index.html")
  @path = File.expand_path(path) if path
  @listing_allowed = listing_allowed
  @index_html = index_html
  @default_content_type = "application/octet-stream".freeze
end

Public Instance Methods

can_serve(path_info) click to toggle source

Checks if the given path can be served and returns the full path (or nil if not).

# File lib/mongrel/handlers.rb, line 129
def can_serve(path_info)

  req_path = HttpRequest.unescape(path_info)
  # Add the drive letter or root path
  req_path = File.join(@path, req_path) if @path
  req_path = File.expand_path req_path
  
  if File.exist? req_path and (!@path or req_path.index(@path) == 0)
    # It exists and it's in the right location
    if File.directory? req_path
      # The request is for a directory
      index = File.join(req_path, @index_html)
      if File.exist? index
        # Serve the index
        return index
      elsif @listing_allowed
        # Serve the directory
        return req_path
      else
        # Do not serve anything
        return nil
      end
    else
      # It's a file and it's there
      return req_path
    end
  else
    # does not exist or isn't in the right spot
    return nil
  end
end
process(request, response) click to toggle source

Process the request to either serve a file or a directory listing if allowed (based on the listing_allowed parameter to the constructor).

# File lib/mongrel/handlers.rb, line 249
def process(request, response)
  req_method = request.params[Const::REQUEST_METHOD] || Const::GET
  req_path = can_serve request.params[Const::PATH_INFO]
  if not req_path
    # not found, return a 404
    response.start(404) do |head,out|
      out << "File not found"
    end
  else
    begin
      if File.directory? req_path
        send_dir_listing(request.params[Const::REQUEST_URI], req_path, response)
      elsif req_method == Const::HEAD
        send_file(req_path, request, response, true)
      elsif req_method == Const::GET
        send_file(req_path, request, response, false)
      else
        response.start(403) {|head,out| out.write(ONLY_HEAD_GET) }
      end
    rescue => details
      STDERR.puts "Error sending file #{req_path}: #{details}"
    end
  end
end
send_dir_listing(base, dir, response) click to toggle source

Returns a simplistic directory listing if they’re enabled, otherwise a 403. Base is the base URI from the REQUEST_URI, dir is the directory to serve on the file system (comes from can_serve()), and response is the HttpResponse object to send the results on.

# File lib/mongrel/handlers.rb, line 166
def send_dir_listing(base, dir, response)
  # take off any trailing / so the links come out right
  base = HttpRequest.unescape(base)
  base.chop! if base[-1] == "/"[-1]

  if @listing_allowed
    response.start(200) do |head,out|
      head[Const::CONTENT_TYPE] = "text/html"
      out << "<html><head><title>Directory Listing</title></head><body>"
      Dir.entries(dir).each do |child|
        next if child == "."
        out << "<a href=\"#{base}/#{ HttpRequest.escape(child)}\">"
        out << (child == ".." ? "Up to parent.." : child)
        out << "</a><br/>"
      end
      out << "</body></html>"
    end
  else
    response.start(403) do |head,out|
      out.write("Directory listings not allowed")
    end
  end
end
send_file(req_path, request, response, header_only=false) click to toggle source

Sends the contents of a file back to the user. Not terribly efficient since it’s opening and closing the file for each read.

# File lib/mongrel/handlers.rb, line 193
def send_file(req_path, request, response, header_only=false)

  stat = File.stat(req_path)

  # Set the last modified times as well and etag for all files
  mtime = stat.mtime
  # Calculated the same as apache, not sure how well the works on win32
  etag = Const::ETAG_FORMAT % [mtime.to_i, stat.size, stat.ino]

  modified_since = request.params[Const::HTTP_IF_MODIFIED_SINCE]
  none_match = request.params[Const::HTTP_IF_NONE_MATCH]

  # test to see if this is a conditional request, and test if
  # the response would be identical to the last response
  same_response = case
                  when modified_since && !last_response_time = Time.httpdate(modified_since) rescue nil then false
                  when modified_since && last_response_time > Time.now                                  then false
                  when modified_since && mtime > last_response_time                                     then false
                  when none_match     && none_match == '*'                                              then false
                  when none_match     && !none_match.strip.split(/\s*,\s*/).include?(etag)              then false
                  else modified_since || none_match  # validation successful if we get this far and at least one of the header exists
                  end

  header = response.header
  header[Const::ETAG] = etag

  if same_response
    response.start(304) {}
  else
    
    # First we setup the headers and status then we do a very fast send on the socket directly
    
    # Support custom responses except 404, which is the default. A little awkward. 
    response.status = 200 if response.status == 404        
    header[Const::LAST_MODIFIED] = mtime.httpdate

    # Set the mime type from our map based on the ending
    dot_at = req_path.rindex('.')
    if dot_at
      header[Const::CONTENT_TYPE] = MIME_TYPES[req_path[dot_at .. -1]] || @default_content_type
    else
      header[Const::CONTENT_TYPE] = @default_content_type
    end

    # send a status with out content length
    response.send_status(stat.size)
    response.send_header

    if not header_only
      response.send_file(req_path, stat.size < Const::CHUNK_SIZE * 2)
    end
  end
end

[Validate]

Generated with the Darkfish Rdoc Generator 2.