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/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
|
||||||
|
|
|
@ -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'
|
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
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
|
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"
|
||||||
|
|
Loading…
Reference in a new issue