class 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.
Constants
- MIME_TYPES
- MIME_TYPES_FILE
- ONLY_HEAD_GET
Attributes
Public Class Methods
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
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
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 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
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
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 : false when modified_since && last_response_time > Time.now : false when modified_since && mtime > last_response_time : false when none_match && none_match == '*' : false when none_match && !none_match.strip.split(/\s*,\s*/).include?(etag) : 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