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.
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 |
default_content_type | [RW] | |
default_content_type | [RW] | |
path | [R] | |
path | [R] |
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 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.
# 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
# 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
# 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
Checks if the given path can be served and returns the full path (or nil if not).
# 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).
# 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).
# 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).
# 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.
# 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.
# 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.
# 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.
# 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