1
0
Fork 0
mirror of https://github.com/puma/puma.git synced 2022-11-09 13:48:40 -05:00

Remove HttpRequest via simplification

This commit is contained in:
Evan Phoenix 2011-09-23 23:26:13 -07:00
parent f5c534a65e
commit 340d2fbf8e
5 changed files with 105 additions and 187 deletions

View file

@ -25,8 +25,8 @@ require 'thread'
require 'puma/command' require 'puma/command'
require 'puma/configurator' require 'puma/configurator'
require 'puma/const' require 'puma/const'
require 'puma/http_request'
require 'puma/server' require 'puma/server'
require 'puma/utils'
# Puma module containing all of the classes (include C extensions) # Puma module containing all of the classes (include C extensions)
# for running a Puma web server. It contains a minimalist HTTP server # for running a Puma web server. It contains a minimalist HTTP server

View file

@ -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

View file

@ -2,30 +2,6 @@ require 'rack'
require 'puma/thread_pool' require 'puma/thread_pool'
module Puma 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 class Server
include Puma::Const include Puma::Const
@ -45,7 +21,7 @@ module Puma
# Creates a working server on host:port (strange things happen if port # Creates a working server on host:port (strange things happen if port
# isn't a Number). # 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. # join the thread that's processing incoming requests on the socket.
# #
# +concurrent+ indicates how many concurrent requests should be run at # +concurrent+ indicates how many concurrent requests should be run at
@ -261,21 +237,62 @@ module Puma
return @acceptor return @acceptor
end end
def process(env, client, body) def read_body(env, client, body)
begin content_length = env[CONTENT_LENGTH].to_i
request = HttpRequest.new(env, client, body)
# in the case of large file uploads the user could close remain = content_length - body.size
# the socket, so skip those requests
rescue BodyReadError => e return StringIO.new(body) if remain <= 0
return
# 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 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 begin
env["SCRIPT_NAME"] = "" env["SCRIPT_NAME"] = ""
env["rack.version"] = Rack::VERSION env["rack.version"] = Rack::VERSION
env["rack.input"] = request.body env["rack.input"] = body
env["rack.errors"] = $stderr env["rack.errors"] = $stderr
env["rack.multithread"] = true env["rack.multithread"] = true
env["rack.multiprocess"] = false env["rack.multiprocess"] = false
@ -285,7 +302,7 @@ module Puma
env["CONTENT_TYPE"] ||= "" env["CONTENT_TYPE"] ||= ""
env["QUERY_STRING"] ||= "" env["QUERY_STRING"] ||= ""
status, headers, body = @app.call(env) status, headers, res_body = @app.call(env)
client.write "HTTP/1.1 " client.write "HTTP/1.1 "
client.write status.to_s client.write status.to_s
@ -296,29 +313,29 @@ module Puma
colon = ": " colon = ": "
line_ending = "\r\n" line_ending = "\r\n"
headers.each { |k, vs| headers.each do |k, vs|
vs.split("\n").each { |v| vs.split("\n").each do |v|
client.write k client.write k
client.write colon client.write colon
client.write v client.write v
client.write line_ending client.write line_ending
} end
} end
client.write line_ending client.write line_ending
if body.kind_of? String if res_body.kind_of? String
client.write body client.write body
client.flush client.flush
else else
body.each do |part| res-body.each do |part|
client.write part client.write part
client.flush client.flush
end end
end end
ensure ensure
request.close_body body.close
body.close if body.respond_to? :close res_body.close if res_body.respond_to? :close
end end
end end

42
lib/puma/utils.rb Normal file
View file

@ -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

View file

@ -134,13 +134,13 @@ class HttpParserTest < Test::Unit::TestCase
def test_query_parse 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["zed"], "didn't get the request right"
assert res["frank"], "no frank" assert res["frank"], "no frank"
assert_equal "1", res["zed"], "wrong result" 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["zed"], "didn't get the request right"
assert res["frank"], "no frank" assert res["frank"], "no frank"
assert_equal 4,res["zed"].length, "wrong number for zed" assert_equal 4,res["zed"].length, "wrong number for zed"