diff --git a/lib/puma.rb b/lib/puma.rb index a9af16e7..5992c27e 100644 --- a/lib/puma.rb +++ b/lib/puma.rb @@ -25,8 +25,8 @@ require 'thread' require 'puma/command' require 'puma/configurator' require 'puma/const' -require 'puma/http_request' require 'puma/server' +require 'puma/utils' # Puma module containing all of the classes (include C extensions) # for running a Puma web server. It contains a minimalist HTTP server diff --git a/lib/puma/http_request.rb b/lib/puma/http_request.rb deleted file mode 100644 index 53446272..00000000 --- a/lib/puma/http_request.rb +++ /dev/null @@ -1,141 +0,0 @@ - -module Puma - # - # 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. - # - 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, body) - @params = params - @socket = socket - content_length = @params[Const::CONTENT_LENGTH].to_i - - remain = content_length - body.size - - # 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 - 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::PUMA_TMP_BASE) - @body.binmode - else - # small body, just use that - @body = StringIO.new - end - - @body.write body - - read_body remain, content_length - end - - @body.rewind if @body - end - - # 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 - chunk = read_socket(remain % Const::CHUNK_SIZE) - - remain -= @body.write(chunk) - - # 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 - chunk = read_socket(Const::CHUNK_SIZE) - remain -= @body.write(chunk) - end - rescue RuntimeError - # any errors means we should delete the file, including if the - # file is dumped - @socket.close rescue nil - close_body - - raise BodyReadError - end - end - - def close_body - @body.close! if @body.kind_of? IO - end - - def read_socket(len) - if @socket.closed? - raise "Socket already closed when reading." - else - 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 - 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 diff --git a/lib/puma/server.rb b/lib/puma/server.rb index a1938f2c..b46c8c4f 100644 --- a/lib/puma/server.rb +++ b/lib/puma/server.rb @@ -2,30 +2,6 @@ require 'rack' require 'puma/thread_pool' module Puma - # Thrown at a thread when it is timed out. - class TimeoutError < RuntimeError; end - - # This is the main driver of Puma, while the Puma::HttpParser and - # Puma::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 to do the heavy lifting with - # the IO and Ruby. - # - # You use it by doing the following: - # - # server = HttpServer.new("0.0.0.0", 3000) - # server.register("/stuff", MyNiftyHandler.new) - # server.run.join - # - # The last line can be just server.run if you don't want to join the - # thread used. If you don't though Ruby will mysteriously just exit on you. - # - # Ruby's thread implementation is "interesting" to say the least. - # Experiments with *many* different types of IO processing simply cannot - # make a dent in it. Future releases of Puma will find other creative - # ways to make threads faster, but don't hold your breath until Ruby 1.9 - # is actually finally useful. - class Server include Puma::Const @@ -45,7 +21,7 @@ module Puma # Creates a working server on host:port (strange things happen if port # isn't a Number). # - # Use HttpServer.run to start the server and HttpServer.acceptor.join to + # Use HttpServer#run to start the server and HttpServer#acceptor.join to # join the thread that's processing incoming requests on the socket. # # +concurrent+ indicates how many concurrent requests should be run at @@ -261,21 +237,62 @@ module Puma return @acceptor end - def process(env, client, body) - begin - request = HttpRequest.new(env, client, body) + def read_body(env, client, body) + content_length = env[CONTENT_LENGTH].to_i - # in the case of large file uploads the user could close - # the socket, so skip those requests - rescue BodyReadError => e - return + remain = content_length - body.size + + return StringIO.new(body) if remain <= 0 + + # Use a Tempfile if there is a lot of data left + if remain > MAX_BODY + stream = Tempfile.new(Const::PUMA_TMP_BASE) + stream.binmode + else + stream = StringIO.new end + stream.write body + + # Read an odd sized chunk so we can read even sized ones + # after this + chunk = client.read(remain % CHUNK_SIZE) + + # No chunk means a closed socket + unless chunk + stream.close + return nil + end + + remain -= stream.write(chunk) + + # Raed the rest of the chunks + while remain > 0 + chunk = client.read(CHUNK_SIZE) + unless chunk + stream.close + return nil + end + + remain -= stream.write(chunk) + end + + stream.rewind + + return stream + end + + def process(env, client, body) + + body = read_body env, client, body + + return unless body + begin env["SCRIPT_NAME"] = "" env["rack.version"] = Rack::VERSION - env["rack.input"] = request.body + env["rack.input"] = body env["rack.errors"] = $stderr env["rack.multithread"] = true env["rack.multiprocess"] = false @@ -285,7 +302,7 @@ module Puma env["CONTENT_TYPE"] ||= "" env["QUERY_STRING"] ||= "" - status, headers, body = @app.call(env) + status, headers, res_body = @app.call(env) client.write "HTTP/1.1 " client.write status.to_s @@ -296,29 +313,29 @@ module Puma colon = ": " line_ending = "\r\n" - headers.each { |k, vs| - vs.split("\n").each { |v| + headers.each do |k, vs| + vs.split("\n").each do |v| client.write k client.write colon client.write v client.write line_ending - } - } + end + end client.write line_ending - if body.kind_of? String + if res_body.kind_of? String client.write body client.flush else - body.each do |part| + res-body.each do |part| client.write part client.flush end end ensure - request.close_body - body.close if body.respond_to? :close + body.close + res_body.close if res_body.respond_to? :close end end diff --git a/lib/puma/utils.rb b/lib/puma/utils.rb new file mode 100644 index 00000000..2728d4f4 --- /dev/null +++ b/lib/puma/utils.rb @@ -0,0 +1,42 @@ +module Puma + module Utils + # 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 diff --git a/test/test_http11.rb b/test/test_http11.rb index a37e8936..bc3a24c8 100644 --- a/test/test_http11.rb +++ b/test/test_http11.rb @@ -134,13 +134,13 @@ class HttpParserTest < Test::Unit::TestCase def test_query_parse - res = HttpRequest.query_parse("zed=1&frank=#{HttpRequest.escape('&&& ')}") + res = Utils.query_parse("zed=1&frank=#{Utils.escape('&&& ')}") assert res["zed"], "didn't get the request right" assert res["frank"], "no frank" assert_equal "1", res["zed"], "wrong result" - assert_equal "&&& ", HttpRequest.unescape(res["frank"]), "wrong result" + assert_equal "&&& ", Utils.unescape(res["frank"]), "wrong result" - res = HttpRequest.query_parse("zed=1&zed=2&zed=3&frank=11;zed=45") + res = Utils.query_parse("zed=1&zed=2&zed=3&frank=11;zed=45") assert res["zed"], "didn't get the request right" assert res["frank"], "no frank" assert_equal 4,res["zed"].length, "wrong number for zed"