diff options
author | Evan Weaver <eweaver@twitter.com> | 2009-01-31 12:02:36 -0800 |
---|---|---|
committer | Evan Weaver <eweaver@twitter.com> | 2009-01-31 12:02:36 -0800 |
commit | 30458a6d2a1bb30ac0de24f8a6131bc568adfac7 (patch) | |
tree | 71b3314a791aeef32dc4022845f5552f82a49e23 /lib | |
parent | 3e1c8c363126814b60c164922ffa26b8227defda (diff) | |
parent | 0d838c607c0c709e5190b24aff116306f4d02255 (diff) | |
download | unicorn-30458a6d2a1bb30ac0de24f8a6131bc568adfac7.tar.gz |
Diffstat (limited to 'lib')
-rw-r--r-- | lib/mongrel.rb | 78 | ||||
-rw-r--r-- | lib/mongrel/configurator.rb | 388 | ||||
-rw-r--r-- | lib/mongrel/handlers.rb | 236 | ||||
-rw-r--r-- | lib/mongrel/header_out.rb | 10 | ||||
-rw-r--r-- | lib/mongrel/http_request.rb | 53 | ||||
-rw-r--r-- | lib/mongrel/http_response.rb | 24 | ||||
-rw-r--r-- | lib/mongrel/uri_classifier.rb | 76 |
7 files changed, 68 insertions, 797 deletions
diff --git a/lib/mongrel.rb b/lib/mongrel.rb index f09a617..e0c2b01 100644 --- a/lib/mongrel.rb +++ b/lib/mongrel.rb @@ -17,14 +17,13 @@ require 'mongrel/gems' Mongrel::Gems.require 'cgi_multipart_eof_fix' Mongrel::Gems.require 'fastthread' require 'thread' +require 'rack' # Ruby Mongrel require 'mongrel/cgi' require 'mongrel/handlers' require 'mongrel/command' require 'mongrel/tcphack' -require 'mongrel/configurator' -require 'mongrel/uri_classifier' require 'mongrel/const' require 'mongrel/http_request' require 'mongrel/header_out' @@ -88,21 +87,20 @@ module Mongrel # The throttle parameter is a sleep timeout (in hundredths of a second) that is placed between # socket.accept calls in order to give the server a cheap throttle time. It defaults to 0 and # actually if it is 0 then the sleep is not done at all. - def initialize(host, port, num_processors=950, throttle=0, timeout=60) - + def initialize(host, port, app, opts = {}) tries = 0 @socket = TCPServer.new(host, port) if defined?(Fcntl::FD_CLOEXEC) @socket.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) end - - @classifier = URIClassifier.new @host = host @port = port @workers = ThreadGroup.new - @throttle = throttle / 100.0 - @num_processors = num_processors - @timeout = timeout + # Set default opts + @app = app + @num_processors = opts.delete(:num_processors) || 950 + @throttle = (opts.delete(:throttle) || 0) / 100 + @timeout = opts.delete(:timeout) || 60 end # Does the majority of the IO processing. It has been written in Ruby using @@ -134,46 +132,25 @@ module Mongrel raise "No REQUEST PATH" if not params[Const::REQUEST_PATH] - script_name, path_info, handlers = @classifier.resolve(params[Const::REQUEST_PATH]) - - if handlers - params[Const::PATH_INFO] = path_info - params[Const::SCRIPT_NAME] = script_name - - # From http://www.ietf.org/rfc/rfc3875 : - # "Script authors should be aware that the REMOTE_ADDR and REMOTE_HOST - # meta-variables (see sections 4.1.8 and 4.1.9) may not identify the - # ultimate source of the request. They identify the client for the - # immediate request to the server; that client may be a proxy, gateway, - # or other intermediary acting on behalf of the actual source client." - params[Const::REMOTE_ADDR] = client.peeraddr.last - - # select handlers that want more detailed request notification - notifiers = handlers.select { |h| h.request_notify } - request = HttpRequest.new(params, client, notifiers) - - # in the case of large file uploads the user could close the socket, so skip those requests - break if request.body == nil # nil signals from HttpRequest::initialize that the request was aborted - - # request is good so far, continue processing the response - response = HttpResponse.new(client) - - # Process each handler in registered order until we run out or one finalizes the response. - handlers.each do |handler| - handler.process(request, response) - break if response.done or client.closed? - end - - # And finally, if nobody closed the response off, we finalize it. - unless response.done or client.closed? - response.finished - end - else - # Didn't find it, return a stock 404 response. - client.write(Const::ERROR_404_RESPONSE) - end - - break #done + params[Const::PATH_INFO] = params[Const::REQUEST_PATH] + params[Const::SCRIPT_NAME] = Const::SLASH + + # From http://www.ietf.org/rfc/rfc3875 : + # "Script authors should be aware that the REMOTE_ADDR and REMOTE_HOST + # meta-variables (see sections 4.1.8 and 4.1.9) may not identify the + # ultimate source of the request. They identify the client for the + # immediate request to the server; that client may be a proxy, gateway, + # or other intermediary acting on behalf of the actual source client." + params[Const::REMOTE_ADDR] = client.peeraddr.last + + # select handlers that want more detailed request notification + request = HttpRequest.new(params, client) + + # in the case of large file uploads the user could close the socket, so skip those requests + break if request.body == nil # nil signals from HttpRequest::initialize that the request was aborted + app_response = @app.call(request.env) + response = HttpResponse.new(client, app_response).start + break #done else # Parser is not done, queue up more data to read and continue parsing chunk = client.readpartial(Const::CHUNK_SIZE) @@ -260,7 +237,7 @@ module Mongrel # Runs the thing. It returns the thread used so you can "join" it. You can also # access the HttpServer::acceptor attribute to get the thread later. - def run + def start! BasicSocket.do_not_reverse_lookup=true configure_socket_options @@ -280,7 +257,6 @@ module Mongrel end worker_list = @workers.list - if worker_list.length >= @num_processors STDERR.puts "Server overloaded with #{worker_list.length} processors (#@num_processors max). Dropping connection." client.close rescue nil diff --git a/lib/mongrel/configurator.rb b/lib/mongrel/configurator.rb deleted file mode 100644 index 439b44c..0000000 --- a/lib/mongrel/configurator.rb +++ /dev/null @@ -1,388 +0,0 @@ -require 'yaml' -require 'etc' - -module Mongrel - # Implements a simple DSL for configuring a Mongrel server for your - # purposes. More used by framework implementers to setup Mongrel - # how they like, but could be used by regular folks to add more things - # to an existing mongrel configuration. - # - # It is used like this: - # - # require 'mongrel' - # config = Mongrel::Configurator.new :host => "127.0.0.1" do - # listener :port => 3000 do - # uri "/app", :handler => Mongrel::DirHandler.new(".", load_mime_map("mime.yaml")) - # end - # run - # end - # - # This will setup a simple DirHandler at the current directory and load additional - # mime types from mimy.yaml. The :host => "127.0.0.1" is actually not - # specific to the servers but just a hash of default parameters that all - # server or uri calls receive. - # - # When you are inside the block after Mongrel::Configurator.new you can simply - # call functions that are part of Configurator (like server, uri, daemonize, etc) - # without having to refer to anything else. You can also call these functions on - # the resulting object directly for additional configuration. - # - # A major thing about Configurator is that it actually lets you configure - # multiple listeners for any hosts and ports you want. These are kept in a - # map config.listeners so you can get to them. - # - # * :pid_file => Where to write the process ID. - class Configurator - attr_reader :listeners - attr_reader :defaults - attr_reader :needs_restart - - # You pass in initial defaults and then a block to continue configuring. - def initialize(defaults={}, &block) - @listener = nil - @listener_name = nil - @listeners = {} - @defaults = defaults - @needs_restart = false - @pid_file = defaults[:pid_file] - - if block - cloaker(&block).bind(self).call - end - end - - # Change privileges of the process to specified user and group. - def change_privilege(user, group) - begin - uid, gid = Process.euid, Process.egid - target_uid = Etc.getpwnam(user).uid if user - target_gid = Etc.getgrnam(group).gid if group - - if uid != target_uid or gid != target_gid - log "Initiating groups for #{user.inspect}:#{group.inspect}." - Process.initgroups(user, target_gid) - - log "Changing group to #{group.inspect}." - Process::GID.change_privilege(target_gid) - - log "Changing user to #{user.inspect}." - Process::UID.change_privilege(target_uid) - end - rescue Errno::EPERM => e - log "Couldn't change user and group to #{user.inspect}:#{group.inspect}: #{e.to_s}." - log "Mongrel failed to start." - exit 1 - end - end - - def remove_pid_file - File.unlink(@pid_file) if @pid_file and File.exists?(@pid_file) - end - - # Writes the PID file if we're not on Windows. - def write_pid_file - if RUBY_PLATFORM !~ /djgpp|(cyg|ms|bcc)win|mingw/ - log "Writing PID file to #{@pid_file}" - open(@pid_file,"w") {|f| f.write(Process.pid) } - open(@pid_file,"w") do |f| - f.write(Process.pid) - File.chmod(0644, @pid_file) - end - end - end - - # Generates a class for cloaking the current self and making the DSL nicer. - def cloaking_class - class << self - self - end - end - - # Do not call this. You were warned. - def cloaker(&block) - cloaking_class.class_eval do - define_method :cloaker_, &block - meth = instance_method( :cloaker_ ) - remove_method :cloaker_ - meth - end - end - - # This will resolve the given options against the defaults. - # Normally just used internally. - def resolve_defaults(options) - options.merge(@defaults) - end - - # Starts a listener block. This is the only one that actually takes - # a block and then you make Configurator.uri calls in order to setup - # your URIs and handlers. If you write your Handlers as GemPlugins - # then you can use load_plugins and plugin to load them. - # - # It expects the following options (or defaults): - # - # * :host => Host name to bind. - # * :port => Port to bind. - # * :num_processors => The maximum number of concurrent threads allowed. - # * :throttle => Time to pause (in hundredths of a second) between accepting clients. - # * :timeout => Time to wait (in seconds) before killing a stalled thread. - # * :user => User to change to, must have :group as well. - # * :group => Group to change to, must have :user as well. - # - def listener(options={},&block) - raise "Cannot call listener inside another listener block." if (@listener or @listener_name) - ops = resolve_defaults(options) - ops[:num_processors] ||= 950 - ops[:throttle] ||= 0 - ops[:timeout] ||= 60 - - @listener = Mongrel::HttpServer.new(ops[:host], ops[:port].to_i, ops[:num_processors].to_i, ops[:throttle].to_i, ops[:timeout].to_i) - @listener_name = "#{ops[:host]}:#{ops[:port]}" - @listeners[@listener_name] = @listener - - if ops[:user] and ops[:group] - change_privilege(ops[:user], ops[:group]) - end - - # Does the actual cloaking operation to give the new implicit self. - if block - cloaker(&block).bind(self).call - end - - # all done processing this listener setup, reset implicit variables - @listener = nil - @listener_name = nil - end - - - # Called inside a Configurator.listener block in order to - # add URI->handler mappings for that listener. Use this as - # many times as you like. It expects the following options - # or defaults: - # - # * :handler => HttpHandler -- Handler to use for this location. - # * :in_front => true/false -- Rather than appending, it prepends this handler. - def uri(location, options={}) - ops = resolve_defaults(options) - @listener.register(location, ops[:handler], ops[:in_front]) - end - - - # Daemonizes the current Ruby script turning all the - # listeners into an actual "server" or detached process. - # You must call this *before* frameworks that open files - # as otherwise the files will be closed by this function. - # - # Does not work for Win32 systems (the call is silently ignored). - # - # Requires the following options or defaults: - # - # * :cwd => Directory to change to. - # * :log_file => Where to write STDOUT and STDERR. - # - # It is safe to call this on win32 as it will only require the daemons - # gem/library if NOT win32. - def daemonize(options={}) - ops = resolve_defaults(options) - # save this for later since daemonize will hose it - if RUBY_PLATFORM !~ /djgpp|(cyg|ms|bcc)win|mingw/ - require 'daemons/daemonize' - - logfile = ops[:log_file] - if logfile[0].chr != "/" - logfile = File.join(ops[:cwd],logfile) - if not File.exist?(File.dirname(logfile)) - log "!!! Log file directory not found at full path #{File.dirname(logfile)}. Update your configuration to use a full path." - exit 1 - end - end - - Daemonize.daemonize(logfile) - - # change back to the original starting directory - Dir.chdir(ops[:cwd]) - - else - log "WARNING: Win32 does not support daemon mode." - end - end - - - # Uses the GemPlugin system to easily load plugins based on their - # gem dependencies. You pass in either an :includes => [] or - # :excludes => [] setting listing the names of plugins to include - # or exclude from the determining the dependencies. - def load_plugins(options={}) - ops = resolve_defaults(options) - - load_settings = {} - if ops[:includes] - ops[:includes].each do |plugin| - load_settings[plugin] = GemPlugin::INCLUDE - end - end - - if ops[:excludes] - ops[:excludes].each do |plugin| - load_settings[plugin] = GemPlugin::EXCLUDE - end - end - - GemPlugin::Manager.instance.load(load_settings) - end - - - # Easy way to load a YAML file and apply default settings. - def load_yaml(file, default={}) - default.merge(YAML.load_file(file)) - end - - - # Loads the MIME map file and checks that it is correct - # on loading. This is commonly passed to Mongrel::DirHandler - # or any framework handler that uses DirHandler to serve files. - # You can also include a set of default MIME types as additional - # settings. See Mongrel::DirHandler for how the MIME types map - # is organized. - def load_mime_map(file, mime={}) - # configure any requested mime map - mime = load_yaml(file, mime) - - # check all the mime types to make sure they are the right format - mime.each {|k,v| log "WARNING: MIME type #{k} must start with '.'" if k.index(".") != 0 } - - return mime - end - - - # Loads and creates a plugin for you based on the given - # name and configured with the selected options. The options - # are merged with the defaults prior to passing them in. - def plugin(name, options={}) - ops = resolve_defaults(options) - GemPlugin::Manager.instance.create(name, ops) - end - - # Lets you do redirects easily as described in Mongrel::RedirectHandler. - # You use it inside the configurator like this: - # - # redirect("/test", "/to/there") # simple - # redirect("/to", /t/, 'w') # regexp - # redirect("/hey", /(w+)/) {|match| ...} # block - # - def redirect(from, pattern, replacement = nil, &block) - uri from, :handler => Mongrel::RedirectHandler.new(pattern, replacement, &block) - end - - # Works like a meta run method which goes through all the - # configured listeners. Use the Configurator.join method - # to prevent Ruby from exiting until each one is done. - def run - @listeners.each {|name,s| - s.run - } - - $mongrel_sleeper_thread = Thread.new { loop { sleep 1 } } - end - - # Calls .stop on all the configured listeners so they - # stop processing requests (gracefully). By default it - # assumes that you don't want to restart. - def stop(needs_restart=false, synchronous=false) - @listeners.each do |name,s| - s.stop(synchronous) - end - @needs_restart = needs_restart - end - - - # This method should actually be called *outside* of the - # Configurator block so that you can control it. In other words - # do it like: config.join. - def join - @listeners.values.each {|s| s.acceptor.join } - end - - - # Calling this before you register your URIs to the given location - # will setup a set of handlers that log open files, objects, and the - # parameters for each request. This helps you track common problems - # found in Rails applications that are either slow or become unresponsive - # after a little while. - # - # You can pass an extra parameter *what* to indicate what you want to - # debug. For example, if you just want to dump rails stuff then do: - # - # debug "/", what = [:rails] - # - # And it will only produce the log/mongrel_debug/rails.log file. - # Available options are: :access, :files, :objects, :threads, :rails - # - # NOTE: Use [:files] to get accesses dumped to stderr like with WEBrick. - def debug(location, what = [:access, :files, :objects, :threads, :rails]) - require 'mongrel/debug' - handlers = { - :access => "/handlers/requestlog::access", - :files => "/handlers/requestlog::files", - :objects => "/handlers/requestlog::objects", - :threads => "/handlers/requestlog::threads", - :rails => "/handlers/requestlog::params" - } - - # turn on the debugging infrastructure, and ObjectTracker is a pig - MongrelDbg.configure - - # now we roll through each requested debug type, turn it on and load that plugin - what.each do |type| - MongrelDbg.begin_trace type - uri location, :handler => plugin(handlers[type]) - end - end - - # Used to allow you to let users specify their own configurations - # inside your Configurator setup. You pass it a script name and - # it reads it in and does an eval on the contents passing in the right - # binding so they can put their own Configurator statements. - def run_config(script) - open(script) {|f| eval(f.read, proc {self}) } - end - - # Sets up the standard signal handlers that are used on most Ruby - # It only configures if the platform is not win32 and doesn't do - # a HUP signal since this is typically framework specific. - # - # Requires a :pid_file option given to Configurator.new to indicate a file to delete. - # It sets the MongrelConfig.needs_restart attribute if - # the start command should reload. It's up to you to detect this - # and do whatever is needed for a "restart". - # - # This command is safely ignored if the platform is win32 (with a warning) - def setup_signals(options={}) - ops = resolve_defaults(options) - - # forced shutdown, even if previously restarted (actually just like TERM but for CTRL-C) - trap("INT") { log "INT signal received."; stop(false) } - - # clean up the pid file always - at_exit { remove_pid_file } - - if RUBY_PLATFORM !~ /djgpp|(cyg|ms|bcc)win|mingw/ - # graceful shutdown - trap("TERM") { log "TERM signal received."; stop } - trap("USR1") { log "USR1 received, toggling $mongrel_debug_client to #{!$mongrel_debug_client}"; $mongrel_debug_client = !$mongrel_debug_client } - # restart - trap("USR2") { log "USR2 signal received."; stop(true) } - - log "Signals ready. TERM => stop. USR2 => restart. INT => stop (no restart)." - else - log "Signals ready. INT => stop (no restart)." - end - end - - # Logs a simple message to STDERR (or the mongrel log if in daemon mode). - def log(msg) - STDERR.print "** ", msg, "\n" - end - - end -end diff --git a/lib/mongrel/handlers.rb b/lib/mongrel/handlers.rb index e643025..ce24628 100644 --- a/lib/mongrel/handlers.rb +++ b/lib/mongrel/handlers.rb @@ -88,198 +88,6 @@ module Mongrel 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. - # - # 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. - # - class DirHandler < HttpHandler - attr_accessor :default_content_type - attr_reader :path - - 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 - - # You give it the path to the directory root and and optional listing_allowed and index_html - 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 - - # Checks if the given path can be served and returns the full path (or nil if not). - 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 - - - # 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 = 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 - - - # 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_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 : false - when modified_since && last_response_time > Time.now : false - when modified_since && mtime > last_response_time : false - when none_match && none_match == '*' : false - when none_match && !none_match.strip.split(/\s*,\s*/).include?(etag) : 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 - - # Process the request to either serve a file or a directory listing - # if allowed (based on the listing_allowed parameter to the constructor). - 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 - - # 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 - - # When added to a config script (-S in mongrel_rails) it will # look at the client's allowed response types and then gzip # compress anything that is going out. @@ -421,48 +229,4 @@ module Mongrel end end end - - # This handler allows you to redirect one url to another. - # You can use it like String#gsub, where the string is the REQUEST_URI. - # REQUEST_URI is the full path with GET parameters. - # - # Eg. /test/something?help=true&disclaimer=false - # - # == Examples - # - # h = Mongrel::HttpServer.new('0.0.0.0') - # h.register '/test', Mongrel::RedirectHandler.new('/to/there') # simple - # h.register '/to', Mongrel::RedirectHandler.new(/t/, 'w') # regexp - # # and with a block - # h.register '/hey', Mongrel::RedirectHandler.new(/(\w+)/) { |match| ... } - # - class RedirectHandler < Mongrel::HttpHandler - # You set the rewrite rules when building the object. - # - # pattern => What to look for or replacement if used alone - # - # replacement, block => One of them is used to replace the found text - - def initialize(pattern, replacement = nil, &block) - unless replacement or block - @pattern, @replacement = nil, pattern - else - @pattern, @replacement, @block = pattern, replacement, block - end - end - - # Process the request and return a redirect response - def process(request, response) - unless @pattern - response.socket.write(Mongrel::Const::REDIRECT % @replacement) - else - if @block - new_path = request.params['REQUEST_URI'].gsub(@pattern, &@block) - else - new_path = request.params['REQUEST_URI'].gsub(@pattern, @replacement) - end - response.socket.write(Mongrel::Const::REDIRECT % new_path) - end - end - end end diff --git a/lib/mongrel/header_out.rb b/lib/mongrel/header_out.rb index b34e95e..008bff8 100644 --- a/lib/mongrel/header_out.rb +++ b/lib/mongrel/header_out.rb @@ -10,13 +10,19 @@ module Mongrel attr_reader :out attr_accessor :allowed_duplicates - def initialize(out) + def initialize(out = StringIO.new) @sent = {} @allowed_duplicates = {"Set-Cookie" => true, "Set-Cookie2" => true, "Warning" => true, "WWW-Authenticate" => true} @out = out end + def merge!(hash) + hash.each do |key, value| + self[key] = value + end + end + # Simply writes "#{key}: #{value}" to an output buffer. def[]=(key,value) if not @sent.has_key?(key) or @allowed_duplicates.has_key?(key) @@ -25,4 +31,4 @@ module Mongrel end end end -end
\ No newline at end of file +end diff --git a/lib/mongrel/http_request.rb b/lib/mongrel/http_request.rb index c8d4ce4..2416b04 100644 --- a/lib/mongrel/http_request.rb +++ b/lib/mongrel/http_request.rb @@ -1,45 +1,27 @@ module Mongrel # - # When a handler is found for a registered URI then this class is constructed - # and passed to your HttpHandler::process method. You should assume that - # *one* handler processes all requests. Included in the HttpRequest is a - # HttpRequest.params Hash that matches common CGI params, and a HttpRequest.body - # which is a string containing the request body (raw for now). - # # The HttpRequest.initialize method will convert any request that is larger than # Const::MAX_BODY into a Tempfile and use that as the body. Otherwise it uses # a StringIO object. To be safe, you should assume it works like a file. - # - # The HttpHandler.request_notify system is implemented by having HttpRequest call - # HttpHandler.request_begins, HttpHandler.request_progress, HttpHandler.process during - # the IO processing. This adds a small amount of overhead but lets you implement - # finer controlled handlers and filters. - # + # class HttpRequest attr_reader :body, :params # You don't really call this. It's made for you. # Main thing it does is hook up the params, and store any remaining # body data into the HttpRequest.body attribute. - def initialize(params, socket, dispatchers) + def initialize(params, socket) @params = params @socket = socket - @dispatchers = dispatchers content_length = @params[Const::CONTENT_LENGTH].to_i remain = content_length - @params.http_body.length - - # tell all dispatchers the request has begun - @dispatchers.each do |dispatcher| - dispatcher.request_begins(@params) - end unless @dispatchers.nil? || @dispatchers.empty? # Some clients (like FF1.0) report 0 for body and then send a body. This will probably truncate them but at least the request goes through usually. if remain <= 0 # we've got everything, pack it up @body = StringIO.new @body.write @params.http_body - update_request_progress(0, content_length) elsif remain > 0 # must read more data to complete body if remain > Const::MAX_BODY @@ -58,14 +40,25 @@ module Mongrel @body.rewind if @body end - # updates all dispatchers about our progress - def update_request_progress(clen, total) - return if @dispatchers.nil? || @dispatchers.empty? - @dispatchers.each do |dispatcher| - dispatcher.request_progress(@params, clen, total) - end + # returns an environment which is rackable + # http://rack.rubyforge.org/doc/files/SPEC.html + # copied directly from racks mongrel handler + def env + env = params.clone + env.delete "HTTP_CONTENT_TYPE" + env.delete "HTTP_CONTENT_LENGTH" + env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/" + env.update({"rack.version" => [0,1], + "rack.input" => @body, + "rack.errors" => STDERR, + + "rack.multithread" => true, + "rack.multiprocess" => false, # ??? + "rack.run_once" => false, + + "rack.url_scheme" => "http", + }) end - private :update_request_progress # Does the heavy lifting of properly reading the larger body requests in # small chunks. It expects @body to be an IO object, @socket to be valid, @@ -78,15 +71,11 @@ module Mongrel remain -= @body.write(@params.http_body) - update_request_progress(remain, total) - # then stream out nothing but perfectly sized chunks until remain <= 0 or @socket.closed? # ASSUME: we are writing to a disk and these writes always write the requested amount @params.http_body = read_socket(Const::CHUNK_SIZE) remain -= @body.write(@params.http_body) - - update_request_progress(remain, total) end rescue Object => e STDERR.puts "#{Time.now}: Error reading HTTP body: #{e.inspect}" @@ -152,4 +141,4 @@ module Mongrel return params end end -end
\ No newline at end of file +end diff --git a/lib/mongrel/http_response.rb b/lib/mongrel/http_response.rb index 3076712..811570b 100644 --- a/lib/mongrel/http_response.rb +++ b/lib/mongrel/http_response.rb @@ -39,13 +39,15 @@ module Mongrel attr_reader :header_sent attr_reader :status_sent - def initialize(socket) + def initialize(socket, app_response) @socket = socket - @body = StringIO.new - @status = 404 + @app_response = app_response + @body = StringIO.new(app_response[2].join('')) + @status = app_response[0] @reason = nil - @header = HeaderOut.new(StringIO.new) + @header = HeaderOut.new @header[Const::DATE] = Time.now.httpdate + @header.merge!(app_response[1]) @body_sent = false @header_sent = false @status_sent = false @@ -59,11 +61,9 @@ module Mongrel # by simple passing "finalize=true" to the start method. By default # all handlers run and then mongrel finalizes the request when they're # all done. - def start(status=200, finalize=false, reason=nil) - @status = status.to_i - @reason = reason - yield @header, @body - finished if finalize + # TODO: docs + def start #(status=200, finalize=false, reason=nil) + finished end # Primarily used in exception handling to reset the response output in order to write @@ -78,7 +78,7 @@ module Mongrel # XXX Dubious ( http://mongrel.rubyforge.org/ticket/19 ) @header.out.close @header = HeaderOut.new(StringIO.new) - + @body.close @body = StringIO.new end @@ -87,7 +87,7 @@ module Mongrel def send_status(content_length=@body.length) if not @status_sent @header['Content-Length'] = content_length if content_length and @status != 304 - write(Const::STATUS_FORMAT % [@status, @reason || HTTP_STATUS_CODES[@status]]) + write(Const::STATUS_FORMAT % [@status, HTTP_STATUS_CODES[@status]]) @status_sent = true end end @@ -163,4 +163,4 @@ module Mongrel end end -end
\ No newline at end of file +end diff --git a/lib/mongrel/uri_classifier.rb b/lib/mongrel/uri_classifier.rb deleted file mode 100644 index f39ccc9..0000000 --- a/lib/mongrel/uri_classifier.rb +++ /dev/null @@ -1,76 +0,0 @@ - -module Mongrel - class URIClassifier - - class RegistrationError < RuntimeError - end - class UsageError < RuntimeError - end - - attr_reader :handler_map - - # Returns the URIs that have been registered with this classifier so far. - def uris - @handler_map.keys - end - - def initialize - @handler_map = {} - @matcher = // - @root_handler = nil - end - - # Register a handler object at a particular URI. The handler can be whatever - # you want, including an array. It's up to you what to do with it. - # - # Registering a handler is not necessarily threadsafe, so be careful if you go - # mucking around once the server is running. - def register(uri, handler) - raise RegistrationError, "#{uri.inspect} is already registered" if @handler_map[uri] - raise RegistrationError, "URI is empty" if !uri or uri.empty? - raise RegistrationError, "URI must begin with a \"#{Const::SLASH}\"" unless uri[0..0] == Const::SLASH - @handler_map[uri.dup] = handler - rebuild - end - - # Unregister a particular URI and its handler. - def unregister(uri) - handler = @handler_map.delete(uri) - raise RegistrationError, "#{uri.inspect} was not registered" unless handler - rebuild - handler - end - - # Resolve a request URI by finding the best partial match in the registered - # handler URIs. - def resolve(request_uri) - if @root_handler - # Optimization for the pathological case of only one handler on "/"; e.g. Rails - [Const::SLASH, request_uri, @root_handler] - elsif match = @matcher.match(request_uri) - uri = match.to_s - # A root mounted ("/") handler must resolve such that path info matches the original URI. - [uri, (uri == Const::SLASH ? request_uri : match.post_match), @handler_map[uri]] - else - [nil, nil, nil] - end - end - - private - - def rebuild - if @handler_map.size == 1 and @handler_map[Const::SLASH] - @root_handler = @handler_map.values.first - else - @root_handler = nil - routes = @handler_map.keys.sort.sort_by do |uri| - -uri.length - end - @matcher = Regexp.new(routes.map do |uri| - Regexp.new('^' + Regexp.escape(uri)) - end.join('|')) - end - end - - end -end
\ No newline at end of file |