about summary refs log tree commit homepage
path: root/lib/mongrel/http_request.rb
diff options
context:
space:
mode:
authorevanweaver <evanweaver@19e92222-5c0b-0410-8929-a290d50e31e9>2007-10-26 09:23:10 +0000
committerevanweaver <evanweaver@19e92222-5c0b-0410-8929-a290d50e31e9>2007-10-26 09:23:10 +0000
commit14d8cca4f2f92858cc76f39403392d0e49fe587c (patch)
tree0eedc907453b7806dbc01f114f86dceba6d6d4d7 /lib/mongrel/http_request.rb
parent1b1d758124a0f30e0a331716e6684973ca2bc94b (diff)
downloadunicorn-14d8cca4f2f92858cc76f39403392d0e49fe587c.tar.gz
git-svn-id: svn+ssh://rubyforge.org/var/svn/mongrel/trunk@766 19e92222-5c0b-0410-8929-a290d50e31e9
Diffstat (limited to 'lib/mongrel/http_request.rb')
-rw-r--r--lib/mongrel/http_request.rb155
1 files changed, 155 insertions, 0 deletions
diff --git a/lib/mongrel/http_request.rb b/lib/mongrel/http_request.rb
new file mode 100644
index 0000000..82ffe42
--- /dev/null
+++ b/lib/mongrel/http_request.rb
@@ -0,0 +1,155 @@
+
+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)
+      @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
+          # huge body, put it in a tempfile
+          @body = Tempfile.new(Const::MONGREL_TMP_BASE)
+          @body.binmode
+        else
+          # small body, just use that
+          @body = StringIO.new
+        end
+
+        @body.write @params.http_body
+        read_body(remain, content_length)
+      end
+
+      @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
+    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,
+    # and will set @body = nil if the request fails.  It also expects any initial
+    # part of the body that has been read to be in the @body already.
+    def read_body(remain, total)
+      begin
+        # write the odd sized chunk first
+        @params.http_body = read_socket(remain % Const::CHUNK_SIZE)
+
+        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}"
+        STDERR.puts e.backtrace.join("\n")
+        # any errors means we should delete the file, including if the file is dumped
+        @socket.close rescue nil
+        @body.delete if @body.class == Tempfile
+        @body = nil # signals that there was a problem
+      end
+    end
+
+    def read_socket(len)
+      if !@socket.closed?
+        data = @socket.read(len)
+        if !data
+          raise "Socket read return nil"
+        elsif data.length != len
+          raise "Socket read returned insufficient data: #{data.length}"
+        else
+          data
+        end
+      else
+        raise "Socket already closed when reading."
+      end
+    end
+
+    # Performs URI escaping so that you can construct proper
+    # query strings faster.  Use this rather than the cgi.rb
+    # version since it's faster.  (Stolen from Camping).
+    def self.escape(s)
+      s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
+        '%'+$1.unpack('H2'*$1.size).join('%').upcase
+      }.tr(' ', '+')
+    end
+
+
+    # Unescapes a URI escaped string. (Stolen from Camping).
+    def self.unescape(s)
+      s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
+        [$1.delete('%')].pack('H*')
+      }
+    end
+
+    # Parses a query string by breaking it up at the '&'
+    # and ';' characters.  You can also use this to parse
+    # cookies by changing the characters used in the second
+    # parameter (which defaults to '&;'.
+    def self.query_parse(qs, d = '&;')
+      params = {}
+      (qs||'').split(/[#{d}] */n).inject(params) { |h,p|
+        k, v=unescape(p).split('=',2)
+        if cur = params[k]
+          if cur.class == Array
+            params[k] << v
+          else
+            params[k] = [cur, v]
+          end
+        else
+          params[k] = v
+        end
+      }
+
+      return params
+    end
+  end
+end \ No newline at end of file