mirror of
https://github.com/puma/puma.git
synced 2022-11-09 13:48:40 -05:00
7965484434
* Fix a bug that the last CRLF of chunked body may be used in the next request The last CRLF of chunked body is checked by #1607. But it's incomplete. If a client sends the last CRLF (or just LF) after Puma processes "0\r\n" line, the last CRLF (or just LF) isn't dropped in the "0\r\n" process:675344e860/lib/puma/client.rb (L183-L192)
if line.end_with?("\r\n") len = line.strip.to_i(16) if len == 0 @body.rewind rest = io.read # rest is "" with no the last CRLF case and # "\r" with no last LF case. # rest.start_with?("\r\n") returns false for # Both of these cases. rest = rest[2..-1] if rest.start_with?("\r\n") @buffer = rest.empty? ? nil : rest set_ready return true end The unprocessed last CRLF (or LF) is used as the first data in the next request. Because Puma::Client#reset sets `@parsed_bytes` to 0.675344e860/lib/puma/client.rb (L100-L109)
def reset(fast_check=true) @parsed_bytes = 0 It means that data in `@buffer` (it's "\r" in no the last LF case) and unread data in input socket (it's "\r\n" in no the last CRLF case and "\n" in no the last LF case) are used used as the first data in the next request. This change fixes these cases by the followings: * Ensures reading the last CRLF by setting `@partial_part_left` when CRLF isn't read in processing "0\r\n" line. * Introduces a `@in_last_chunk` new state to detect whether the last CRLF is waiting or not. It's reset in Puma::Client#reset. * Remove unnecessary returns https://github.com/puma/puma/pull/1812#discussion_r307806310 is the location where this rule is made. * Add missing last CRLF for chunked request in tests
506 lines
11 KiB
Ruby
506 lines
11 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class IO
|
|
# We need to use this for a jruby work around on both 1.8 and 1.9.
|
|
# So this either creates the constant (on 1.8), or harmlessly
|
|
# reopens it (on 1.9).
|
|
module WaitReadable
|
|
end
|
|
end
|
|
|
|
require 'puma/detect'
|
|
require 'puma/delegation'
|
|
require 'tempfile'
|
|
|
|
if Puma::IS_JRUBY
|
|
# We have to work around some OpenSSL buffer/io-readiness bugs
|
|
# so we pull it in regardless of if the user is binding
|
|
# to an SSL socket
|
|
require 'openssl'
|
|
end
|
|
|
|
module Puma
|
|
|
|
class ConnectionError < RuntimeError; end
|
|
|
|
# An instance of this class represents a unique request from a client.
|
|
# For example a web request from a browser or from CURL. This
|
|
#
|
|
# An instance of `Puma::Client` can be used as if it were an IO object
|
|
# by the reactor, that's because the latter is expected to call `#to_io`
|
|
# on any non-IO objects it polls. For example nio4r internally calls
|
|
# `IO::try_convert` (which may call `#to_io`) when a new socket is
|
|
# registered.
|
|
#
|
|
# Instances of this class are responsible for knowing if
|
|
# the header and body are fully buffered via the `try_to_finish` method.
|
|
# They can be used to "time out" a response via the `timeout_at` reader.
|
|
class Client
|
|
include Puma::Const
|
|
extend Puma::Delegation
|
|
|
|
def initialize(io, env=nil)
|
|
@io = io
|
|
@to_io = io.to_io
|
|
@proto_env = env
|
|
if !env
|
|
@env = nil
|
|
else
|
|
@env = env.dup
|
|
end
|
|
|
|
@parser = HttpParser.new
|
|
@parsed_bytes = 0
|
|
@read_header = true
|
|
@ready = false
|
|
|
|
@body = nil
|
|
@body_read_start = nil
|
|
@buffer = nil
|
|
@tempfile = nil
|
|
|
|
@timeout_at = nil
|
|
|
|
@requests_served = 0
|
|
@hijacked = false
|
|
|
|
@peerip = nil
|
|
@remote_addr_header = nil
|
|
|
|
@body_remain = 0
|
|
|
|
@in_last_chunk = false
|
|
end
|
|
|
|
attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
|
|
:tempfile
|
|
|
|
attr_writer :peerip
|
|
|
|
attr_accessor :remote_addr_header
|
|
|
|
forward :closed?, :@io
|
|
|
|
def inspect
|
|
"#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
|
|
end
|
|
|
|
# For the hijack protocol (allows us to just put the Client object
|
|
# into the env)
|
|
def call
|
|
@hijacked = true
|
|
env[HIJACK_IO] ||= @io
|
|
end
|
|
|
|
def in_data_phase
|
|
!@read_header
|
|
end
|
|
|
|
def set_timeout(val)
|
|
@timeout_at = Time.now + val
|
|
end
|
|
|
|
def reset(fast_check=true)
|
|
@parser.reset
|
|
@read_header = true
|
|
@env = @proto_env.dup
|
|
@body = nil
|
|
@tempfile = nil
|
|
@parsed_bytes = 0
|
|
@ready = false
|
|
@body_remain = 0
|
|
@peerip = nil
|
|
@in_last_chunk = false
|
|
|
|
if @buffer
|
|
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
|
|
|
if @parser.finished?
|
|
return setup_body
|
|
elsif @parsed_bytes >= MAX_HEADER
|
|
raise HttpParserError,
|
|
"HEADER is longer than allowed, aborting client early."
|
|
end
|
|
|
|
return false
|
|
else
|
|
begin
|
|
if fast_check &&
|
|
IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
|
|
return try_to_finish
|
|
end
|
|
rescue IOError
|
|
# swallow it
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
def close
|
|
begin
|
|
@io.close
|
|
rescue IOError
|
|
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
|
|
end
|
|
end
|
|
|
|
# The object used for a request with no body. All requests with
|
|
# no body share this one object since it has no state.
|
|
EmptyBody = NullIO.new
|
|
|
|
def setup_chunked_body(body)
|
|
@chunked_body = true
|
|
@partial_part_left = 0
|
|
@prev_chunk = ""
|
|
|
|
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
|
@body.binmode
|
|
@tempfile = @body
|
|
|
|
return decode_chunk(body)
|
|
end
|
|
|
|
def decode_chunk(chunk)
|
|
if @partial_part_left > 0
|
|
if @partial_part_left <= chunk.size
|
|
if @partial_part_left > 2
|
|
@body << chunk[0..(@partial_part_left-3)] # skip the \r\n
|
|
end
|
|
chunk = chunk[@partial_part_left..-1]
|
|
@partial_part_left = 0
|
|
else
|
|
@body << chunk if @partial_part_left > 2 # don't include the last \r\n
|
|
@partial_part_left -= chunk.size
|
|
return false
|
|
end
|
|
end
|
|
|
|
if @prev_chunk.empty?
|
|
io = StringIO.new(chunk)
|
|
else
|
|
io = StringIO.new(@prev_chunk+chunk)
|
|
@prev_chunk = ""
|
|
end
|
|
|
|
while !io.eof?
|
|
line = io.gets
|
|
if line.end_with?("\r\n")
|
|
len = line.strip.to_i(16)
|
|
if len == 0
|
|
@in_last_chunk = true
|
|
@body.rewind
|
|
rest = io.read
|
|
last_crlf_size = "\r\n".bytesize
|
|
if rest.bytesize < last_crlf_size
|
|
@buffer = nil
|
|
@partial_part_left = last_crlf_size - rest.bytesize
|
|
return false
|
|
else
|
|
@buffer = rest[last_crlf_size..-1]
|
|
@buffer = nil if @buffer.empty?
|
|
set_ready
|
|
return true
|
|
end
|
|
end
|
|
|
|
len += 2
|
|
|
|
part = io.read(len)
|
|
|
|
unless part
|
|
@partial_part_left = len
|
|
next
|
|
end
|
|
|
|
got = part.size
|
|
|
|
case
|
|
when got == len
|
|
@body << part[0..-3] # to skip the ending \r\n
|
|
when got <= len - 2
|
|
@body << part
|
|
@partial_part_left = len - part.size
|
|
when got == len - 1 # edge where we get just \r but not \n
|
|
@body << part[0..-2]
|
|
@partial_part_left = len - part.size
|
|
end
|
|
else
|
|
@prev_chunk = line
|
|
return false
|
|
end
|
|
end
|
|
|
|
if @in_last_chunk
|
|
set_ready
|
|
true
|
|
else
|
|
false
|
|
end
|
|
end
|
|
|
|
def read_chunked_body
|
|
while true
|
|
begin
|
|
chunk = @io.read_nonblock(4096)
|
|
rescue IO::WaitReadable
|
|
return false
|
|
rescue SystemCallError, IOError
|
|
raise ConnectionError, "Connection error detected during read"
|
|
end
|
|
|
|
# No chunk means a closed socket
|
|
unless chunk
|
|
@body.close
|
|
@buffer = nil
|
|
set_ready
|
|
raise EOFError
|
|
end
|
|
|
|
return true if decode_chunk(chunk)
|
|
end
|
|
end
|
|
|
|
def setup_body
|
|
@body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
|
|
|
if @env[HTTP_EXPECT] == CONTINUE
|
|
# TODO allow a hook here to check the headers before
|
|
# going forward
|
|
@io << HTTP_11_100
|
|
@io.flush
|
|
end
|
|
|
|
@read_header = false
|
|
|
|
body = @parser.body
|
|
|
|
te = @env[TRANSFER_ENCODING2]
|
|
|
|
if te && CHUNKED.casecmp(te) == 0
|
|
return setup_chunked_body(body)
|
|
end
|
|
|
|
@chunked_body = false
|
|
|
|
cl = @env[CONTENT_LENGTH]
|
|
|
|
unless cl
|
|
@buffer = body.empty? ? nil : body
|
|
@body = EmptyBody
|
|
set_ready
|
|
return true
|
|
end
|
|
|
|
remain = cl.to_i - body.bytesize
|
|
|
|
if remain <= 0
|
|
@body = StringIO.new(body)
|
|
@buffer = nil
|
|
set_ready
|
|
return true
|
|
end
|
|
|
|
if remain > MAX_BODY
|
|
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
|
@body.binmode
|
|
@tempfile = @body
|
|
else
|
|
# The body[0,0] trick is to get an empty string in the same
|
|
# encoding as body.
|
|
@body = StringIO.new body[0,0]
|
|
end
|
|
|
|
@body.write body
|
|
|
|
@body_remain = remain
|
|
|
|
return false
|
|
end
|
|
|
|
def try_to_finish
|
|
return read_body unless @read_header
|
|
|
|
begin
|
|
data = @io.read_nonblock(CHUNK_SIZE)
|
|
rescue Errno::EAGAIN
|
|
return false
|
|
rescue SystemCallError, IOError, EOFError
|
|
raise ConnectionError, "Connection error detected during read"
|
|
end
|
|
|
|
# No data means a closed socket
|
|
unless data
|
|
@buffer = nil
|
|
set_ready
|
|
raise EOFError
|
|
end
|
|
|
|
if @buffer
|
|
@buffer << data
|
|
else
|
|
@buffer = data
|
|
end
|
|
|
|
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
|
|
|
if @parser.finished?
|
|
return setup_body
|
|
elsif @parsed_bytes >= MAX_HEADER
|
|
raise HttpParserError,
|
|
"HEADER is longer than allowed, aborting client early."
|
|
end
|
|
|
|
false
|
|
end
|
|
|
|
if IS_JRUBY
|
|
def jruby_start_try_to_finish
|
|
return read_body unless @read_header
|
|
|
|
begin
|
|
data = @io.sysread_nonblock(CHUNK_SIZE)
|
|
rescue OpenSSL::SSL::SSLError => e
|
|
return false if e.kind_of? IO::WaitReadable
|
|
raise e
|
|
end
|
|
|
|
# No data means a closed socket
|
|
unless data
|
|
@buffer = nil
|
|
set_ready
|
|
raise EOFError
|
|
end
|
|
|
|
if @buffer
|
|
@buffer << data
|
|
else
|
|
@buffer = data
|
|
end
|
|
|
|
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
|
|
|
if @parser.finished?
|
|
return setup_body
|
|
elsif @parsed_bytes >= MAX_HEADER
|
|
raise HttpParserError,
|
|
"HEADER is longer than allowed, aborting client early."
|
|
end
|
|
|
|
false
|
|
end
|
|
|
|
def eagerly_finish
|
|
return true if @ready
|
|
|
|
if @io.kind_of? OpenSSL::SSL::SSLSocket
|
|
return true if jruby_start_try_to_finish
|
|
end
|
|
|
|
return false unless IO.select([@to_io], nil, nil, 0)
|
|
try_to_finish
|
|
end
|
|
|
|
else
|
|
|
|
def eagerly_finish
|
|
return true if @ready
|
|
return false unless IO.select([@to_io], nil, nil, 0)
|
|
try_to_finish
|
|
end
|
|
end # IS_JRUBY
|
|
|
|
def finish
|
|
return true if @ready
|
|
until try_to_finish
|
|
IO.select([@to_io], nil, nil)
|
|
end
|
|
true
|
|
end
|
|
|
|
def read_body
|
|
if @chunked_body
|
|
return read_chunked_body
|
|
end
|
|
|
|
# Read an odd sized chunk so we can read even sized ones
|
|
# after this
|
|
remain = @body_remain
|
|
|
|
if remain > CHUNK_SIZE
|
|
want = CHUNK_SIZE
|
|
else
|
|
want = remain
|
|
end
|
|
|
|
begin
|
|
chunk = @io.read_nonblock(want)
|
|
rescue Errno::EAGAIN
|
|
return false
|
|
rescue SystemCallError, IOError
|
|
raise ConnectionError, "Connection error detected during read"
|
|
end
|
|
|
|
# No chunk means a closed socket
|
|
unless chunk
|
|
@body.close
|
|
@buffer = nil
|
|
set_ready
|
|
raise EOFError
|
|
end
|
|
|
|
remain -= @body.write(chunk)
|
|
|
|
if remain <= 0
|
|
@body.rewind
|
|
@buffer = nil
|
|
set_ready
|
|
return true
|
|
end
|
|
|
|
@body_remain = remain
|
|
|
|
false
|
|
end
|
|
|
|
def set_ready
|
|
if @body_read_start
|
|
@env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
|
|
end
|
|
@requests_served += 1
|
|
@ready = true
|
|
end
|
|
|
|
def write_400
|
|
begin
|
|
@io << ERROR_400_RESPONSE
|
|
rescue StandardError
|
|
end
|
|
end
|
|
|
|
def write_408
|
|
begin
|
|
@io << ERROR_408_RESPONSE
|
|
rescue StandardError
|
|
end
|
|
end
|
|
|
|
def write_500
|
|
begin
|
|
@io << ERROR_500_RESPONSE
|
|
rescue StandardError
|
|
end
|
|
end
|
|
|
|
def peerip
|
|
return @peerip if @peerip
|
|
|
|
if @remote_addr_header
|
|
hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
|
|
@peerip = hdr
|
|
return hdr
|
|
end
|
|
|
|
@peerip ||= @io.peeraddr.last
|
|
end
|
|
end
|
|
end
|