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:
parent
f5c534a65e
commit
340d2fbf8e
5 changed files with 105 additions and 187 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
42
lib/puma/utils.rb
Normal file
42
lib/puma/utils.rb
Normal 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
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue