mirror of
https://github.com/puma/puma.git
synced 2022-11-09 13:48:40 -05:00
As the CGI and Handlers get bigger we'll need to separate their code out. This change does that.
git-svn-id: svn+ssh://rubyforge.org/var/svn/mongrel/trunk@53 19e92222-5c0b-0410-8929-a290d50e31e9
This commit is contained in:
parent
ffb373d602
commit
e5c2f9404a
5 changed files with 341 additions and 334 deletions
|
@ -121,6 +121,7 @@ class StartCommand < Mongrel::Command::Command
|
|||
server = Mongrel::HttpServer.new(@address, @port, @num_procs.to_i, @timeout.to_i)
|
||||
server.register("/", rails)
|
||||
server.run
|
||||
trap("INT") { server.stop }
|
||||
|
||||
begin
|
||||
puts "Server ready."
|
||||
|
|
|
@ -24,7 +24,7 @@ if ARGV.length != 3
|
|||
exit(1)
|
||||
end
|
||||
|
||||
h = Mongrel::HttpServer.new(ARGV[0], ARGV[1])
|
||||
h = Mongrel::HttpServer.new(ARGV[0], ARGV[1].to_i)
|
||||
h.register("/", SimpleHandler.new)
|
||||
h.register("/files", Mongrel::DirHandler.new(ARGV[2]))
|
||||
h.run
|
||||
|
|
345
lib/mongrel.rb
345
lib/mongrel.rb
|
@ -2,8 +2,8 @@ require 'socket'
|
|||
require 'http11'
|
||||
require 'thread'
|
||||
require 'stringio'
|
||||
require 'cgi'
|
||||
|
||||
require 'mongrel/cgi'
|
||||
require 'mongrel/handlers'
|
||||
|
||||
# Mongrel module containing all of the classes (include C extensions) for running
|
||||
# a Mongrel web server. It contains a minimalist HTTP server with just enough
|
||||
|
@ -156,6 +156,14 @@ module Mongrel
|
|||
# fix up the CGI requirements
|
||||
params[Const::CONTENT_LENGTH] = params[Const::HTTP_CONTENT_LENGTH] || 0
|
||||
params[Const::CONTENT_TYPE] = params[Const::HTTP_CONTENT_TYPE] if params[Const::HTTP_CONTENT_TYPE]
|
||||
params[Const::GATEWAY_INTERFACE]=Const::GATEWAY_INTERFACE_VALUE
|
||||
params[Const::REMOTE_ADDR]=socket.peeraddr[3]
|
||||
host,port = params[Const::HTTP_HOST].split(":")
|
||||
params[Const::SERVER_NAME]=host
|
||||
params[Const::SERVER_PORT]=port if port
|
||||
params[Const::SERVER_PROTOCOL]=Const::SERVER_PROTOCOL_VALUE
|
||||
params[Const::SERVER_SOFTWARE]=Const::MONGREL_VERSION
|
||||
|
||||
|
||||
# now, if the initial_body isn't long enough for the content length we have to fill it
|
||||
# TODO: adapt for big ass stuff by writing to a temp file
|
||||
|
@ -277,16 +285,6 @@ module Mongrel
|
|||
end
|
||||
|
||||
|
||||
# You implement your application handler with this. It's very light giving
|
||||
# just the minimum necessary for you to handle a request and shoot back
|
||||
# a response. Look at the HttpRequest and HttpResponse objects for how
|
||||
# to use them.
|
||||
class HttpHandler
|
||||
def process(request, response)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# This is the main driver of Mongrel, while the Mognrel::HttpParser and Mongrel::URIClassifier
|
||||
# make up the majority of how the server functions. It's a very simple class that just
|
||||
# has a thread accepting connections and a simple HttpServer.process_client function
|
||||
|
@ -362,14 +360,6 @@ module Mongrel
|
|||
if handler
|
||||
params[Const::PATH_INFO] = path_info
|
||||
params[Const::SCRIPT_NAME] = script_name
|
||||
params[Const::GATEWAY_INTERFACE]=Const::GATEWAY_INTERFACE_VALUE
|
||||
params[Const::REMOTE_ADDR]=client.peeraddr[3]
|
||||
host,port = params[Const::HTTP_HOST].split(":")
|
||||
params[Const::SERVER_NAME]=host
|
||||
params[Const::SERVER_PORT]=port if port
|
||||
params[Const::SERVER_PROTOCOL]=Const::SERVER_PROTOCOL_VALUE
|
||||
params[Const::SERVER_SOFTWARE]=Const::MONGREL_VERSION
|
||||
|
||||
request = HttpRequest.new(params, data[nread ... data.length], client)
|
||||
response = HttpResponse.new(client)
|
||||
handler.process(request, response)
|
||||
|
@ -456,317 +446,6 @@ module Mongrel
|
|||
|
||||
end
|
||||
|
||||
|
||||
# The server normally returns a 404 response if a URI is requested, but it
|
||||
# also returns a lame empty message. This lets you do a 404 response
|
||||
# with a custom message for special URIs.
|
||||
class Error404Handler < HttpHandler
|
||||
|
||||
# Sets the message to return. This is constructed once for the handler
|
||||
# so it's pretty efficient.
|
||||
def initialize(msg)
|
||||
@response = Const::ERROR_404_RESPONSE + msg
|
||||
end
|
||||
|
||||
# Just kicks back the standard 404 response with your special message.
|
||||
def process(request, response)
|
||||
response.socket.write(@response)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
# 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.
|
||||
class DirHandler < HttpHandler
|
||||
MIME_TYPES = {
|
||||
".css" => "text/css",
|
||||
".gif" => "image/gif",
|
||||
".htm" => "text/html",
|
||||
".html" => "text/html",
|
||||
".jpeg" => "image/jpeg",
|
||||
".jpg" => "image/jpeg",
|
||||
".js" => "text/javascript",
|
||||
".png" => "image/png",
|
||||
".swf" => "application/x-shockwave-flash",
|
||||
".txt" => "text/plain"
|
||||
}
|
||||
|
||||
|
||||
attr_reader :path
|
||||
|
||||
# You give it the path to the directory root and an (optional)
|
||||
def initialize(path, listing_allowed=true, index_html="index.html")
|
||||
@path = File.expand_path(path)
|
||||
@listing_allowed=listing_allowed
|
||||
@index_html = index_html
|
||||
end
|
||||
|
||||
# Checks if the given path can be served and returns the full path (or nil if not).
|
||||
def can_serve(path_info)
|
||||
req = File.expand_path(File.join(@path,path_info), @path)
|
||||
|
||||
if req.index(@path) == 0 and File.exist? req
|
||||
# it exists and it's in the right location
|
||||
if File.directory? req
|
||||
# the request is for a directory
|
||||
index = File.join(req, @index_html)
|
||||
if File.exist? index
|
||||
# serve the index
|
||||
return index
|
||||
elsif @listing_allowed
|
||||
# serve the directory
|
||||
req
|
||||
else
|
||||
# do not serve anything
|
||||
return nil
|
||||
end
|
||||
else
|
||||
# it's a file and it's there
|
||||
return req
|
||||
end
|
||||
else
|
||||
# does not exist or isn't in the right spot
|
||||
return nil
|
||||
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.
|
||||
def send_dir_listing(base, dir, response)
|
||||
# take off any trailing / so the links come out right
|
||||
base.chop! if base[-1] == "/"[-1]
|
||||
|
||||
if @listing_allowed
|
||||
response.start(200) do |head,out|
|
||||
head['Content-Type'] = "text/html"
|
||||
out << "<html><head><title>Directory Listing</title></head><body>"
|
||||
Dir.entries(dir).each do |child|
|
||||
next if child == "."
|
||||
|
||||
if child == ".."
|
||||
out << "<a href=\"#{base}/#{child}\">Up to parent..</a><br/>"
|
||||
else
|
||||
out << "<a href=\"#{base}/#{child}\">#{child}</a><br/>"
|
||||
end
|
||||
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.
|
||||
def send_file(req, response)
|
||||
response.start(200) do |head,out|
|
||||
# set the mime type from our map based on the ending
|
||||
dot_at = req.rindex(".")
|
||||
if dot_at
|
||||
ext = req[dot_at .. -1]
|
||||
if MIME_TYPES[ext]
|
||||
head['Content-Type'] = MIME_TYPES[ext]
|
||||
end
|
||||
end
|
||||
|
||||
open(req, "rb") do |f|
|
||||
out.write(f.read)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Process the request to either serve a file or a directory listing
|
||||
# if allowed (based on the listing_allowed paramter to the constructor).
|
||||
def process(request, response)
|
||||
req = can_serve request.params['PATH_INFO']
|
||||
if not req
|
||||
# not found, return a 404
|
||||
response.start(404) do |head,out|
|
||||
out << "File not found"
|
||||
end
|
||||
else
|
||||
begin
|
||||
if File.directory? req
|
||||
send_dir_listing(request.params["REQUEST_URI"],req, response)
|
||||
else
|
||||
send_file(req, response)
|
||||
end
|
||||
rescue => details
|
||||
response.reset
|
||||
response.start(403) do |head,out|
|
||||
out << "Error accessing file: #{details}"
|
||||
out << details.backtrace.join("\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
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.
|
||||
def DirHandler::add_mime_type(extension, type)
|
||||
MIME_TYPES[extension] = type
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
# The beginning of a complete wrapper around Mongrel's internal HTTP processing
|
||||
# system but maintaining the original Ruby CGI module. Use this only as a crutch
|
||||
# to get existing CGI based systems working. It should handle everything, but please
|
||||
# notify me if you see special warnings. This work is still very alpha so I need
|
||||
# testers to help work out the various corner cases.
|
||||
class CGIWrapper < ::CGI
|
||||
public :env_table
|
||||
attr_reader :options
|
||||
|
||||
# these are stripped out of any keys passed to CGIWrapper.header function
|
||||
REMOVED_KEYS = [ "nph","status","server","connection","type",
|
||||
"charset","length","language","expires"]
|
||||
|
||||
# Takes an HttpRequest and HttpResponse object, plus any additional arguments
|
||||
# normally passed to CGI. These are used internally to create a wrapper around
|
||||
# the real CGI while maintaining Mongrel's view of the world.
|
||||
def initialize(request, response, *args)
|
||||
@request = request
|
||||
@response = response
|
||||
@args = *args
|
||||
@input = StringIO.new(request.body)
|
||||
@head = {}
|
||||
@out_called = false
|
||||
super(*args)
|
||||
end
|
||||
|
||||
# The header is typically called to send back the header. In our case we
|
||||
# collect it into a hash for later usage.
|
||||
#
|
||||
# nph -- Mostly ignored. It'll output the date.
|
||||
# connection -- Completely ignored. Why is CGI doing this?
|
||||
# length -- Ignored since Mongrel figures this out from what you write to output.
|
||||
#
|
||||
def header(options = "text/html")
|
||||
# if they pass in a string then just write the Content-Type
|
||||
if options.class == String
|
||||
@head['Content-Type'] = options unless @head['Content-Type']
|
||||
else
|
||||
# convert the given options into what Mongrel wants
|
||||
@head['Content-Type'] = options['type'] || "text/html"
|
||||
@head['Content-Type'] += "; charset=" + options['charset'] if options.has_key? "charset" if options['charset']
|
||||
|
||||
# setup date only if they use nph
|
||||
@head['Date'] = CGI::rfc1123_date(Time.now) if options['nph']
|
||||
|
||||
# setup the server to use the default or what they set
|
||||
@head['Server'] = options['server'] || env_table['SERVER_SOFTWARE']
|
||||
|
||||
# remaining possible options they can give
|
||||
@head['Status'] = options['status'] if options['status']
|
||||
@head['Content-Language'] = options['language'] if options['language']
|
||||
@head['Expires'] = options['expires'] if options['expires']
|
||||
|
||||
# drop the keys we don't want anymore
|
||||
REMOVED_KEYS.each {|k| options.delete(k) }
|
||||
|
||||
# finally just convert the rest raw (which puts 'cookie' directly)
|
||||
# 'cookie' is translated later as we write the header out
|
||||
options.each{|k,v| @head[k] = v}
|
||||
end
|
||||
|
||||
# doing this fakes out the cgi library to think the headers are empty
|
||||
# we then do the real headers in the out function call later
|
||||
""
|
||||
end
|
||||
|
||||
# Takes any 'cookie' setting and sends it over the Mongrel header,
|
||||
# then removes the setting from the options. If cookie is an
|
||||
# Array or Hash then it sends those on with .to_s, otherwise
|
||||
# it just calls .to_s on it and hopefully your "cookie" can
|
||||
# write itself correctly.
|
||||
def send_cookies(to)
|
||||
# convert the cookies based on the myriad of possible ways to set a cookie
|
||||
if @head['cookie']
|
||||
cookie = @head['cookie']
|
||||
case cookie
|
||||
when Array
|
||||
cookie.each {|c| to['Set-Cookie'] = c.to_s }
|
||||
when Hash
|
||||
cookie.each_value {|c| to['Set-Cookie'] = c.to_s}
|
||||
else
|
||||
to['Set-Cookie'] = options['cookie'].to_s
|
||||
end
|
||||
|
||||
@head.delete('cookie')
|
||||
|
||||
# @output_cookies seems to never be used, but we'll process it just in case
|
||||
@output_cookies.each {|c| to['Set-Cookie'] = c.to_s } if @output_cookies
|
||||
end
|
||||
end
|
||||
|
||||
# The dumb thing is people can call header or this or both and in any order.
|
||||
# So, we just reuse header and then finalize the HttpResponse the right way.
|
||||
# Status is taken from the various options and converted to what Mongrel needs
|
||||
# via the CGIWrapper.status function.
|
||||
def out(options = "text/html")
|
||||
return if @out_called # don't do this more than once
|
||||
|
||||
header(options)
|
||||
|
||||
@response.start status do |head, out|
|
||||
send_cookies(head)
|
||||
|
||||
@head.each {|k,v| head[k] = v}
|
||||
out.write(yield || "")
|
||||
end
|
||||
end
|
||||
|
||||
# Computes the status once, but lazily so that people who call header twice
|
||||
# don't get penalized. Because CGI insists on including the options status
|
||||
# message in the status we have to do a bit of parsing.
|
||||
def status
|
||||
if not @status
|
||||
stat = @head["Status"]
|
||||
stat = stat.split(' ')[0] if stat
|
||||
|
||||
@status = stat || "200"
|
||||
end
|
||||
|
||||
@status
|
||||
end
|
||||
|
||||
# Used to wrap the normal args variable used inside CGI.
|
||||
def args
|
||||
@args
|
||||
end
|
||||
|
||||
# Used to wrap the normal env_table variable used inside CGI.
|
||||
def env_table
|
||||
@request.params
|
||||
end
|
||||
|
||||
# Used to wrap the normal stdinput variable used inside CGI.
|
||||
def stdinput
|
||||
@input
|
||||
end
|
||||
|
||||
# The stdoutput should be completely bypassed but we'll drop a warning just in case
|
||||
def stdoutput
|
||||
STDERR.puts "WARNING: Your program is doing something not expected. Please tell Zed that stdoutput was used and what software you are running. Thanks."
|
||||
@response.body
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
|
|
147
lib/mongrel/cgi.rb
Normal file
147
lib/mongrel/cgi.rb
Normal file
|
@ -0,0 +1,147 @@
|
|||
require 'cgi'
|
||||
|
||||
module Mongrel
|
||||
# The beginning of a complete wrapper around Mongrel's internal HTTP processing
|
||||
# system but maintaining the original Ruby CGI module. Use this only as a crutch
|
||||
# to get existing CGI based systems working. It should handle everything, but please
|
||||
# notify me if you see special warnings. This work is still very alpha so I need
|
||||
# testers to help work out the various corner cases.
|
||||
class CGIWrapper < ::CGI
|
||||
public :env_table
|
||||
attr_reader :options
|
||||
|
||||
# these are stripped out of any keys passed to CGIWrapper.header function
|
||||
REMOVED_KEYS = [ "nph","status","server","connection","type",
|
||||
"charset","length","language","expires"]
|
||||
|
||||
# Takes an HttpRequest and HttpResponse object, plus any additional arguments
|
||||
# normally passed to CGI. These are used internally to create a wrapper around
|
||||
# the real CGI while maintaining Mongrel's view of the world.
|
||||
def initialize(request, response, *args)
|
||||
@request = request
|
||||
@response = response
|
||||
@args = *args
|
||||
@input = StringIO.new(request.body)
|
||||
@head = {}
|
||||
@out_called = false
|
||||
super(*args)
|
||||
end
|
||||
|
||||
# The header is typically called to send back the header. In our case we
|
||||
# collect it into a hash for later usage.
|
||||
#
|
||||
# nph -- Mostly ignored. It'll output the date.
|
||||
# connection -- Completely ignored. Why is CGI doing this?
|
||||
# length -- Ignored since Mongrel figures this out from what you write to output.
|
||||
#
|
||||
def header(options = "text/html")
|
||||
# if they pass in a string then just write the Content-Type
|
||||
if options.class == String
|
||||
@head['Content-Type'] = options unless @head['Content-Type']
|
||||
else
|
||||
# convert the given options into what Mongrel wants
|
||||
@head['Content-Type'] = options['type'] || "text/html"
|
||||
@head['Content-Type'] += "; charset=" + options['charset'] if options.has_key? "charset" if options['charset']
|
||||
|
||||
# setup date only if they use nph
|
||||
@head['Date'] = CGI::rfc1123_date(Time.now) if options['nph']
|
||||
|
||||
# setup the server to use the default or what they set
|
||||
@head['Server'] = options['server'] || env_table['SERVER_SOFTWARE']
|
||||
|
||||
# remaining possible options they can give
|
||||
@head['Status'] = options['status'] if options['status']
|
||||
@head['Content-Language'] = options['language'] if options['language']
|
||||
@head['Expires'] = options['expires'] if options['expires']
|
||||
|
||||
# drop the keys we don't want anymore
|
||||
REMOVED_KEYS.each {|k| options.delete(k) }
|
||||
|
||||
# finally just convert the rest raw (which puts 'cookie' directly)
|
||||
# 'cookie' is translated later as we write the header out
|
||||
options.each{|k,v| @head[k] = v}
|
||||
end
|
||||
|
||||
# doing this fakes out the cgi library to think the headers are empty
|
||||
# we then do the real headers in the out function call later
|
||||
""
|
||||
end
|
||||
|
||||
# Takes any 'cookie' setting and sends it over the Mongrel header,
|
||||
# then removes the setting from the options. If cookie is an
|
||||
# Array or Hash then it sends those on with .to_s, otherwise
|
||||
# it just calls .to_s on it and hopefully your "cookie" can
|
||||
# write itself correctly.
|
||||
def send_cookies(to)
|
||||
# convert the cookies based on the myriad of possible ways to set a cookie
|
||||
if @head['cookie']
|
||||
cookie = @head['cookie']
|
||||
case cookie
|
||||
when Array
|
||||
cookie.each {|c| to['Set-Cookie'] = c.to_s }
|
||||
when Hash
|
||||
cookie.each_value {|c| to['Set-Cookie'] = c.to_s}
|
||||
else
|
||||
to['Set-Cookie'] = options['cookie'].to_s
|
||||
end
|
||||
|
||||
@head.delete('cookie')
|
||||
|
||||
# @output_cookies seems to never be used, but we'll process it just in case
|
||||
@output_cookies.each {|c| to['Set-Cookie'] = c.to_s } if @output_cookies
|
||||
end
|
||||
end
|
||||
|
||||
# The dumb thing is people can call header or this or both and in any order.
|
||||
# So, we just reuse header and then finalize the HttpResponse the right way.
|
||||
# Status is taken from the various options and converted to what Mongrel needs
|
||||
# via the CGIWrapper.status function.
|
||||
def out(options = "text/html")
|
||||
return if @out_called # don't do this more than once
|
||||
|
||||
header(options)
|
||||
|
||||
@response.start status do |head, out|
|
||||
send_cookies(head)
|
||||
|
||||
@head.each {|k,v| head[k] = v}
|
||||
out.write(yield || "")
|
||||
end
|
||||
end
|
||||
|
||||
# Computes the status once, but lazily so that people who call header twice
|
||||
# don't get penalized. Because CGI insists on including the options status
|
||||
# message in the status we have to do a bit of parsing.
|
||||
def status
|
||||
if not @status
|
||||
stat = @head["Status"]
|
||||
stat = stat.split(' ')[0] if stat
|
||||
|
||||
@status = stat || "200"
|
||||
end
|
||||
|
||||
@status
|
||||
end
|
||||
|
||||
# Used to wrap the normal args variable used inside CGI.
|
||||
def args
|
||||
@args
|
||||
end
|
||||
|
||||
# Used to wrap the normal env_table variable used inside CGI.
|
||||
def env_table
|
||||
@request.params
|
||||
end
|
||||
|
||||
# Used to wrap the normal stdinput variable used inside CGI.
|
||||
def stdinput
|
||||
@input
|
||||
end
|
||||
|
||||
# The stdoutput should be completely bypassed but we'll drop a warning just in case
|
||||
def stdoutput
|
||||
STDERR.puts "WARNING: Your program is doing something not expected. Please tell Zed that stdoutput was used and what software you are running. Thanks."
|
||||
@response.body
|
||||
end
|
||||
end
|
||||
end
|
180
lib/mongrel/handlers.rb
Normal file
180
lib/mongrel/handlers.rb
Normal file
|
@ -0,0 +1,180 @@
|
|||
|
||||
module Mongrel
|
||||
|
||||
# You implement your application handler with this. It's very light giving
|
||||
# just the minimum necessary for you to handle a request and shoot back
|
||||
# a response. Look at the HttpRequest and HttpResponse objects for how
|
||||
# to use them.
|
||||
class HttpHandler
|
||||
def process(request, response)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# The server normally returns a 404 response if an unknown URI is requested, but it
|
||||
# also returns a lame empty message. This lets you do a 404 response
|
||||
# with a custom message for special URIs.
|
||||
class Error404Handler < HttpHandler
|
||||
|
||||
# Sets the message to return. This is constructed once for the handler
|
||||
# so it's pretty efficient.
|
||||
def initialize(msg)
|
||||
@response = Const::ERROR_404_RESPONSE + msg
|
||||
end
|
||||
|
||||
# Just kicks back the standard 404 response with your special message.
|
||||
def process(request, response)
|
||||
response.socket.write(@response)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
# 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.
|
||||
class DirHandler < HttpHandler
|
||||
MIME_TYPES = {
|
||||
".css" => "text/css",
|
||||
".gif" => "image/gif",
|
||||
".htm" => "text/html",
|
||||
".html" => "text/html",
|
||||
".jpeg" => "image/jpeg",
|
||||
".jpg" => "image/jpeg",
|
||||
".js" => "text/javascript",
|
||||
".png" => "image/png",
|
||||
".swf" => "application/x-shockwave-flash",
|
||||
".txt" => "text/plain"
|
||||
}
|
||||
|
||||
|
||||
attr_reader :path
|
||||
|
||||
# You give it the path to the directory root and an (optional)
|
||||
def initialize(path, listing_allowed=true, index_html="index.html")
|
||||
@path = File.expand_path(path)
|
||||
@listing_allowed=listing_allowed
|
||||
@index_html = index_html
|
||||
end
|
||||
|
||||
# Checks if the given path can be served and returns the full path (or nil if not).
|
||||
def can_serve(path_info)
|
||||
req = File.expand_path(File.join(@path,path_info), @path)
|
||||
|
||||
if req.index(@path) == 0 and File.exist? req
|
||||
# it exists and it's in the right location
|
||||
if File.directory? req
|
||||
# the request is for a directory
|
||||
index = File.join(req, @index_html)
|
||||
if File.exist? index
|
||||
# serve the index
|
||||
return index
|
||||
elsif @listing_allowed
|
||||
# serve the directory
|
||||
req
|
||||
else
|
||||
# do not serve anything
|
||||
return nil
|
||||
end
|
||||
else
|
||||
# it's a file and it's there
|
||||
return req
|
||||
end
|
||||
else
|
||||
# does not exist or isn't in the right spot
|
||||
return nil
|
||||
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.
|
||||
def send_dir_listing(base, dir, response)
|
||||
# take off any trailing / so the links come out right
|
||||
base.chop! if base[-1] == "/"[-1]
|
||||
|
||||
if @listing_allowed
|
||||
response.start(200) do |head,out|
|
||||
head['Content-Type'] = "text/html"
|
||||
out << "<html><head><title>Directory Listing</title></head><body>"
|
||||
Dir.entries(dir).each do |child|
|
||||
next if child == "."
|
||||
|
||||
if child == ".."
|
||||
out << "<a href=\"#{base}/#{child}\">Up to parent..</a><br/>"
|
||||
else
|
||||
out << "<a href=\"#{base}/#{child}\">#{child}</a><br/>"
|
||||
end
|
||||
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.
|
||||
def send_file(req, response)
|
||||
response.start(200) do |head,out|
|
||||
# set the mime type from our map based on the ending
|
||||
dot_at = req.rindex(".")
|
||||
if dot_at
|
||||
ext = req[dot_at .. -1]
|
||||
if MIME_TYPES[ext]
|
||||
head['Content-Type'] = MIME_TYPES[ext]
|
||||
end
|
||||
end
|
||||
|
||||
open(req, "rb") do |f|
|
||||
out.write(f.read)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Process the request to either serve a file or a directory listing
|
||||
# if allowed (based on the listing_allowed paramter to the constructor).
|
||||
def process(request, response)
|
||||
req = can_serve request.params['PATH_INFO']
|
||||
if not req
|
||||
# not found, return a 404
|
||||
response.start(404) do |head,out|
|
||||
out << "File not found"
|
||||
end
|
||||
else
|
||||
begin
|
||||
if File.directory? req
|
||||
send_dir_listing(request.params["REQUEST_URI"],req, response)
|
||||
else
|
||||
send_file(req, response)
|
||||
end
|
||||
rescue => details
|
||||
response.reset
|
||||
response.start(403) do |head,out|
|
||||
out << "Error accessing file: #{details}"
|
||||
out << details.backtrace.join("\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
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.
|
||||
def DirHandler::add_mime_type(extension, type)
|
||||
MIME_TYPES[extension] = type
|
||||
end
|
||||
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue