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

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

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

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

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

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

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

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 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 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

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

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

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

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]