Class: Mongrel::DirHandler

Class | Mongrel::DirHandler
In: | lib/mongrel/handlers.rb lib/mongrel/handlers.rb
Parent: | HttpHandler

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.

### Methods

add_mime_type add_mime_type can_serve can_serve new new process process send_dir_listing send_dir_listing send_file send_file

### Constants

MIME_TYPES_FILE | = | "mime_types.yml"
MIME_TYPES | = | YAML.load_file(File.join(File.dirname(__FILE__), MIME_TYPES_FILE))
ONLY_HEAD_GET | = | "Only HEAD and GET allowed.".freeze
MIME_TYPES_FILE | = | "mime_types.yml"
MIME_TYPES | = | YAML.load_file(File.join(File.dirname(__FILE__), MIME_TYPES_FILE))
ONLY_HEAD_GET | = | "Only HEAD and GET allowed.".freeze

### Attributes

default_content_type | [RW] |
default_content_type | [RW] |
path | [R] |
path | [R] |

### Public Class methods

add_mime_type(extension, type)

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

[Source]

         # File lib/mongrel/handlers.rb, line 276
    276:     def DirHandler::add_mime_type(extension, type)
    277:       MIME_TYPES[extension] = type
    278:     end

add_mime_type(extension, type)

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

[Source]

         # File lib/mongrel/handlers.rb, line 276
    276:     def DirHandler::add_mime_type(extension, type)
    277:       MIME_TYPES[extension] = type
    278:     end

new(path, listing_allowed=true, index_html="index.html")

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

[Source]

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

new(path, listing_allowed=true, index_html="index.html")

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

[Source]

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

### Public Instance methods

can_serve(path_info)

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

[Source]

         # File lib/mongrel/handlers.rb, line 129
    129:     def can_serve(path_info)
    130:
    131:       req_path = HttpRequest.unescape(path_info)
    132:       # Add the drive letter or root path
    133:       req_path = File.join(@path, req_path) if @path
    134:       req_path = File.expand_path req_path
    135:
    136:       if File.exist? req_path and (!@path or req_path.index(@path) == 0)
    137:         # It exists and it's in the right location
    138:         if File.directory? req_path
    139:           # The request is for a directory
    140:           index = File.join(req_path, @index_html)
    141:           if File.exist? index
    142:             # Serve the index
    143:             return index
    144:           elsif @listing_allowed
    145:             # Serve the directory
    146:             return req_path
    147:           else
    148:             # Do not serve anything
    149:             return nil
    150:           end
    151:         else
    152:           # It's a file and it's there
    153:           return req_path
    154:         end
    155:       else
    156:         # does not exist or isn't in the right spot
    157:         return nil
    158:       end
    159:     end

can_serve(path_info)

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

[Source]

         # File lib/mongrel/handlers.rb, line 129
    129:     def can_serve(path_info)
    130:
    131:       req_path = HttpRequest.unescape(path_info)
    132:       # Add the drive letter or root path
    133:       req_path = File.join(@path, req_path) if @path
    134:       req_path = File.expand_path req_path
    135:
    136:       if File.exist? req_path and (!@path or req_path.index(@path) == 0)
    137:         # It exists and it's in the right location
    138:         if File.directory? req_path
    139:           # The request is for a directory
    140:           index = File.join(req_path, @index_html)
    141:           if File.exist? index
    142:             # Serve the index
    143:             return index
    144:           elsif @listing_allowed
    145:             # Serve the directory
    146:             return req_path
    147:           else
    148:             # Do not serve anything
    149:             return nil
    150:           end
    151:         else
    152:           # It's a file and it's there
    153:           return req_path
    154:         end
    155:       else
    156:         # does not exist or isn't in the right spot
    157:         return nil
    158:       end
    159:     end

process(request, response)

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

[Source]

         # File lib/mongrel/handlers.rb, line 249
    249:     def process(request, response)
    250:       req_method = request.params[Const::REQUEST_METHOD] || Const::GET
    251:       req_path = can_serve request.params[Const::PATH_INFO]
    252:       if not req_path
    253:         # not found, return a 404
    254:         response.start(404) do |head,out|
    255:           out << "File not found"
    256:         end
    257:       else
    258:         begin
    259:           if File.directory? req_path
    260:             send_dir_listing(request.params[Const::REQUEST_URI], req_path, response)
    261:           elsif req_method == Const::HEAD
    262:             send_file(req_path, request, response, true)
    263:           elsif req_method == Const::GET
    264:             send_file(req_path, request, response, false)
    265:           else
    266:             response.start(403) {|head,out| out.write(ONLY_HEAD_GET) }
    267:           end
    268:         rescue => details
    269:           STDERR.puts "Error sending file #{req_path}: #{details}"
    270:         end
    271:       end
    272:     end

process(request, response)

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

[Source]

         # File lib/mongrel/handlers.rb, line 249
    249:     def process(request, response)
    250:       req_method = request.params[Const::REQUEST_METHOD] || Const::GET
    251:       req_path = can_serve request.params[Const::PATH_INFO]
    252:       if not req_path
    253:         # not found, return a 404
    254:         response.start(404) do |head,out|
    255:           out << "File not found"
    256:         end
    257:       else
    258:         begin
    259:           if File.directory? req_path
    260:             send_dir_listing(request.params[Const::REQUEST_URI], req_path, response)
    261:           elsif req_method == Const::HEAD
    262:             send_file(req_path, request, response, true)
    263:           elsif req_method == Const::GET
    264:             send_file(req_path, request, response, false)
    265:           else
    266:             response.start(403) {|head,out| out.write(ONLY_HEAD_GET) }
    267:           end
    268:         rescue => details
    269:           STDERR.puts "Error sending file #{req_path}: #{details}"
    270:         end
    271:       end
    272:     end

send_dir_listing(base, dir, response)

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.

[Source]

         # File lib/mongrel/handlers.rb, line 166
    166:     def send_dir_listing(base, dir, response)
    167:       # take off any trailing / so the links come out right
    168:       base = HttpRequest.unescape(base)
    169:       base.chop! if base[-1] == "/"[-1]
    170:
    171:       if @listing_allowed
    172:         response.start(200) do |head,out|
    173:           head[Const::CONTENT_TYPE] = "text/html"
    174:           out << "<html><head><title>Directory Listing</title></head><body>"
    175:           Dir.entries(dir).each do |child|
    176:             next if child == "."
    177:             out << "<a href=\"#{base}/#{ HttpRequest.escape(child)}\">"
    178:             out << (child == ".." ? "Up to parent.." : child)
    179:             out << "</a><br/>"
    180:           end
    181:           out << "</body></html>"
    182:         end
    183:       else
    184:         response.start(403) do |head,out|
    185:           out.write("Directory listings not allowed")
    186:         end
    187:       end
    188:     end

send_dir_listing(base, dir, response)

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.

[Source]

         # File lib/mongrel/handlers.rb, line 166
    166:     def send_dir_listing(base, dir, response)
    167:       # take off any trailing / so the links come out right
    168:       base = HttpRequest.unescape(base)
    169:       base.chop! if base[-1] == "/"[-1]
    170:
    171:       if @listing_allowed
    172:         response.start(200) do |head,out|
    173:           head[Const::CONTENT_TYPE] = "text/html"
    174:           out << "<html><head><title>Directory Listing</title></head><body>"
    175:           Dir.entries(dir).each do |child|
    176:             next if child == "."
    177:             out << "<a href=\"#{base}/#{ HttpRequest.escape(child)}\">"
    178:             out << (child == ".." ? "Up to parent.." : child)
    179:             out << "</a><br/>"
    180:           end
    181:           out << "</body></html>"
    182:         end
    183:       else
    184:         response.start(403) do |head,out|
    185:           out.write("Directory listings not allowed")
    186:         end
    187:       end
    188:     end

send_file(req_path, request, response, header_only=false)

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

[Source]

         # File lib/mongrel/handlers.rb, line 193
    193:     def send_file(req_path, request, response, header_only=false)
    194:
    195:       stat = File.stat(req_path)
    196:
    197:       # Set the last modified times as well and etag for all files
    198:       mtime = stat.mtime
    199:       # Calculated the same as apache, not sure how well the works on win32
    200:       etag = Const::ETAG_FORMAT % [mtime.to_i, stat.size, stat.ino]
    201:
    202:       modified_since = request.params[Const::HTTP_IF_MODIFIED_SINCE]
    203:       none_match = request.params[Const::HTTP_IF_NONE_MATCH]
    204:
    205:       # test to see if this is a conditional request, and test if
    206:       # the response would be identical to the last response
    207:       same_response = case
    208:                       when modified_since && !last_response_time = Time.httpdate(modified_since) rescue nil : false
    209:                       when modified_since && last_response_time > Time.now                                  : false
    210:                       when modified_since && mtime > last_response_time                                     : false
    211:                       when none_match     && none_match == '*'                                              : false
    212:                       when none_match     && !none_match.strip.split(/\s*,\s*/).include?(etag)              : false
    213:                       else modified_since || none_match  # validation successful if we get this far and at least one of the header exists
    214:                       end
    215:
    216:       header = response.header
    217:       header[Const::ETAG] = etag
    218:
    219:       if same_response
    220:         response.start(304) {}
    221:       else
    222:
    223:         # First we setup the headers and status then we do a very fast send on the socket directly
    224:
    225:         # Support custom responses except 404, which is the default. A little awkward.
    226:         response.status = 200 if response.status == 404
    227:         header[Const::LAST_MODIFIED] = mtime.httpdate
    228:
    229:         # Set the mime type from our map based on the ending
    230:         dot_at = req_path.rindex('.')
    231:         if dot_at
    232:           header[Const::CONTENT_TYPE] = MIME_TYPES[req_path[dot_at .. -1]] || @default_content_type
    233:         else
    234:           header[Const::CONTENT_TYPE] = @default_content_type
    235:         end
    236:
    237:         # send a status with out content length
    238:         response.send_status(stat.size)
    239:         response.send_header
    240:
    241:         if not header_only
    242:           response.send_file(req_path, stat.size < Const::CHUNK_SIZE * 2)
    243:         end
    244:       end
    245:     end

send_file(req_path, request, response, header_only=false)

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

[Source]

         # File lib/mongrel/handlers.rb, line 193
    193:     def send_file(req_path, request, response, header_only=false)
    194:
    195:       stat = File.stat(req_path)
    196:
    197:       # Set the last modified times as well and etag for all files
    198:       mtime = stat.mtime
    199:       # Calculated the same as apache, not sure how well the works on win32
    200:       etag = Const::ETAG_FORMAT % [mtime.to_i, stat.size, stat.ino]
    201:
    202:       modified_since = request.params[Const::HTTP_IF_MODIFIED_SINCE]
    203:       none_match = request.params[Const::HTTP_IF_NONE_MATCH]
    204:
    205:       # test to see if this is a conditional request, and test if
    206:       # the response would be identical to the last response
    207:       same_response = case
    208:                       when modified_since && !last_response_time = Time.httpdate(modified_since) rescue nil : false
    209:                       when modified_since && last_response_time > Time.now                                  : false
    210:                       when modified_since && mtime > last_response_time                                     : false
    211:                       when none_match     && none_match == '*'                                              : false
    212:                       when none_match     && !none_match.strip.split(/\s*,\s*/).include?(etag)              : false
    213:                       else modified_since || none_match  # validation successful if we get this far and at least one of the header exists
    214:                       end
    215:
    216:       header = response.header
    217:       header[Const::ETAG] = etag
    218:
    219:       if same_response
    220:         response.start(304) {}
    221:       else
    222:
    223:         # First we setup the headers and status then we do a very fast send on the socket directly
    224:
    225:         # Support custom responses except 404, which is the default. A little awkward.
    226:         response.status = 200 if response.status == 404
    227:         header[Const::LAST_MODIFIED] = mtime.httpdate
    228:
    229:         # Set the mime type from our map based on the ending
    230:         dot_at = req_path.rindex('.')
    231:         if dot_at
    232:           header[Const::CONTENT_TYPE] = MIME_TYPES[req_path[dot_at .. -1]] || @default_content_type
    233:         else
    234:           header[Const::CONTENT_TYPE] = @default_content_type
    235:         end
    236:
    237:         # send a status with out content length
    238:         response.send_status(stat.size)
    239:         response.send_header
    240:
    241:         if not header_only
    242:           response.send_file(req_path, stat.size < Const::CHUNK_SIZE * 2)
    243:         end
    244:       end
    245:     end

[Validate]