Class: Mongrel::DirHandler

Inherits:
HttpHandler show all
Defined in:
lib/mongrel/handlers.rb

Overview

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.

Constant Summary collapse

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

Instance Attribute Summary collapse

Attributes inherited from HttpHandler

#listener, #request_notify

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from HttpHandler

#request_begins, #request_progress

Constructor Details

#initialize(path, listing_allowed = true, index_html = "index.html") ⇒ DirHandler

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


121
122
123
124
125
126
# 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

Instance Attribute Details

#default_content_typeObject

Returns the value of attribute default_content_type


112
113
114
# File 'lib/mongrel/handlers.rb', line 112

def default_content_type
  @default_content_type
end

#pathObject (readonly)

Returns the value of attribute path


113
114
115
# File 'lib/mongrel/handlers.rb', line 113

def path
  @path
end

Class Method Details

.add_mime_type(extension, type) ⇒ Object

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


276
277
278
# File 'lib/mongrel/handlers.rb', line 276

def DirHandler::add_mime_type(extension, type)
  MIME_TYPES[extension] = type
end

Instance Method Details

#can_serve(path_info) ⇒ Object

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


129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# 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) ⇒ Object

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


249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# 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) ⇒ Object

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.


166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# 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) ⇒ Object

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


193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# 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