From e5c2f9404a4864cadc6d01bf174a4c33c39fadc0 Mon Sep 17 00:00:00 2001 From: zedshaw Date: Tue, 21 Feb 2006 00:55:39 +0000 Subject: 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 --- bin/mongrel_rails | 1 + examples/simpletest.rb | 2 +- lib/mongrel.rb | 343 ++---------------------------------------------- lib/mongrel/cgi.rb | 147 +++++++++++++++++++++ lib/mongrel/handlers.rb | 180 +++++++++++++++++++++++++ 5 files changed, 340 insertions(+), 333 deletions(-) create mode 100644 lib/mongrel/cgi.rb create mode 100644 lib/mongrel/handlers.rb diff --git a/bin/mongrel_rails b/bin/mongrel_rails index ed391b1..8c3ccc5 100644 --- a/bin/mongrel_rails +++ b/bin/mongrel_rails @@ -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." diff --git a/examples/simpletest.rb b/examples/simpletest.rb index e7a1011..438f69e 100644 --- a/examples/simpletest.rb +++ b/examples/simpletest.rb @@ -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 diff --git a/lib/mongrel.rb b/lib/mongrel.rb index 239172d..d4d3941 100644 --- a/lib/mongrel.rb +++ b/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 +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 << "Directory Listing" - Dir.entries(dir).each do |child| - next if child == "." - - if child == ".." - out << "Up to parent..
" - else - out << "#{child}
" - end - end - out << "" - 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 diff --git a/lib/mongrel/cgi.rb b/lib/mongrel/cgi.rb new file mode 100644 index 0000000..3d7d25a --- /dev/null +++ b/lib/mongrel/cgi.rb @@ -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 diff --git a/lib/mongrel/handlers.rb b/lib/mongrel/handlers.rb new file mode 100644 index 0000000..609a252 --- /dev/null +++ b/lib/mongrel/handlers.rb @@ -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 << "Directory Listing" + Dir.entries(dir).each do |child| + next if child == "." + + if child == ".." + out << "Up to parent..
" + else + out << "#{child}
" + end + end + out << "" + 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 -- cgit v1.2.3-24-ge0c7