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

499 lines
12 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
class IO
2012-08-10 22:35:47 -04:00
# 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
2012-08-10 22:35:47 -04:00
require 'puma/detect'
require 'tempfile'
require 'forwardable'
2012-08-10 22:35:47 -04:00
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
2018-05-01 16:42:05 -04:00
# An instance of this class represents a unique request from a client.
# For example, this could be a web request from a browser or from CURL.
2018-05-01 16:42:05 -04:00
#
# An instance of `Puma::Client` can be used as if it were an IO object
# by the reactor. The reactor 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.
2018-05-01 16:42:05 -04:00
#
# 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
2019-09-20 07:30:22 -04:00
# 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
include Puma::Const
extend Forwardable
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
2012-07-23 17:29:33 -04:00
@ready = false
@body = nil
@body_read_start = nil
@buffer = nil
@tempfile = nil
@timeout_at = nil
@requests_served = 0
@hijacked = false
2016-01-06 13:12:09 -05:00
@peerip = nil
@remote_addr_header = nil
@body_remain = 0
Fix a bug that the last CRLF of chunked body may be used in the next request (#1812) * 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: https://github.com/puma/puma/blob/675344e8609509b0d767ae7680436b3b382d8394/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. https://github.com/puma/puma/blob/675344e8609509b0d767ae7680436b3b382d8394/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
2019-08-03 18:52:09 -04:00
@in_last_chunk = false
end
attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
:tempfile
2016-01-06 13:12:09 -05:00
attr_writer :peerip
attr_accessor :remote_addr_header
def_delegators :@io, :closed?
# Test to see if io meets a bare minimum of functioning, @to_io needs to be
# used for MiniSSL::Socket
def io_ok?
@to_io.is_a?(::BasicSocket) && !closed?
end
# @!attribute [r] inspect
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
# @!attribute [r] in_data_phase
2014-01-30 17:37:38 -05:00
def in_data_phase
!@read_header
end
def set_timeout(val)
@timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + val
end
# Number of seconds until the timeout elapses.
def timeout
[@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
end
def reset(fast_check=true)
@parser.reset
@read_header = true
@env = @proto_env.dup
@body = nil
@tempfile = nil
@parsed_bytes = 0
2012-07-23 17:29:33 -04:00
@ready = false
@body_remain = 0
@peerip = nil
Fix a bug that the last CRLF of chunked body may be used in the next request (#1812) * 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: https://github.com/puma/puma/blob/675344e8609509b0d767ae7680436b3b382d8394/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. https://github.com/puma/puma/blob/675344e8609509b0d767ae7680436b3b382d8394/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
2019-08-03 18:52:09 -04:00
@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
2012-07-24 20:24:44 -04:00
begin
@io.close
rescue IOError
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
2012-07-24 20:24:44 -04:00
end
end
def try_to_finish
return read_body unless @read_header
2012-08-23 01:34:10 -04:00
begin
data = @io.read_nonblock(CHUNK_SIZE)
rescue IO::WaitReadable
2012-08-23 01:34:10 -04:00
return false
rescue EOFError
# Swallow error, don't log
rescue SystemCallError, IOError
raise ConnectionError, "Connection error detected during read"
2012-08-23 01:34:10 -04:00
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
2016-09-01 17:57:38 -04:00
false
end
def eagerly_finish
return true if @ready
return false unless IO.select([@to_io], nil, nil, 0)
try_to_finish
end
2012-07-23 17:29:33 -04:00
def finish(timeout)
return if @ready
IO.select([@to_io], nil, nil, timeout) || timeout! until try_to_finish
end
def timeout!
write_error(408) if in_data_phase
raise ConnectionError
end
2019-09-20 07:30:22 -04:00
2019-09-20 07:41:58 -04:00
def write_error(status_code)
2019-09-20 07:30:22 -04:00
begin
2019-09-20 07:41:58 -04:00
@io << ERROR_RESPONSE[status_code]
2019-09-20 07:30:22 -04:00
rescue StandardError
end
end
def peerip
return @peerip if @peerip
if @remote_addr_header
2020-03-11 14:49:29 -04:00
hdr = (@env[@remote_addr_header] || LOCALHOST_IP).split(/[\s,]/).first
2019-09-20 07:30:22 -04:00
@peerip = hdr
return hdr
end
@peerip ||= @io.peeraddr.last
end
# Returns true if the persistent connection can be closed immediately
# without waiting for the configured idle/shutdown timeout.
# @version 5.0.0
#
def can_close?
# Allow connection to close if we're not in the middle of parsing a request.
@parsed_bytes == 0
end
2019-09-20 07:30:22 -04:00
private
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]
2020-05-18 19:01:53 -04:00
if te
if te.include?(",")
te.split(",").each do |part|
if CHUNKED.casecmp(part.strip) == 0
return setup_chunked_body(body)
end
end
elsif CHUNKED.casecmp(te) == 0
return setup_chunked_body(body)
end
2019-09-20 07:30:22 -04:00
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 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
2012-08-23 01:34:10 -04:00
begin
chunk = @io.read_nonblock(want)
rescue IO::WaitReadable
2012-08-23 01:34:10 -04:00
return false
rescue SystemCallError, IOError
raise ConnectionError, "Connection error detected during read"
2012-08-23 01:34:10 -04:00
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
2019-09-20 07:30:22 -04:00
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
if decode_chunk(chunk)
@env[CONTENT_LENGTH] = @chunked_content_length
return true
end
end
end
2019-09-20 07:30:22 -04:00
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
@chunked_content_length = 0
if decode_chunk(body)
@env[CONTENT_LENGTH] = @chunked_content_length
return true
end
end
2019-09-20 07:30:22 -04:00
# @version 5.0.0
def write_chunk(str)
@chunked_content_length += @body.write(str)
end
2019-09-20 07:30:22 -04:00
def decode_chunk(chunk)
if @partial_part_left > 0
if @partial_part_left <= chunk.size
if @partial_part_left > 2
write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
2019-09-20 07:30:22 -04:00
end
chunk = chunk[@partial_part_left..-1]
@partial_part_left = 0
else
2020-07-30 10:07:55 -04:00
if @partial_part_left > 2
if @partial_part_left == chunk.size + 1
# Don't include the last \r
write_chunk(chunk[0..(@partial_part_left-3)])
else
# don't include the last \r\n
write_chunk(chunk)
end
end
2019-09-20 07:30:22 -04:00
@partial_part_left -= chunk.size
return false
end
2014-01-30 13:23:01 -05:00
end
2019-09-20 07:30:22 -04:00
if @prev_chunk.empty?
io = StringIO.new(chunk)
else
io = StringIO.new(@prev_chunk+chunk)
@prev_chunk = ""
end
2016-01-06 13:12:09 -05:00
2019-09-20 07:30:22 -04:00
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
2016-01-06 13:12:09 -05:00
2019-09-20 07:30:22 -04:00
len += 2
part = io.read(len)
unless part
@partial_part_left = len
next
end
got = part.size
case
when got == len
write_chunk(part[0..-3]) # to skip the ending \r\n
2019-09-20 07:30:22 -04:00
when got <= len - 2
write_chunk(part)
2019-09-20 07:30:22 -04:00
@partial_part_left = len - part.size
when got == len - 1 # edge where we get just \r but not \n
write_chunk(part[0..-2])
2019-09-20 07:30:22 -04:00
@partial_part_left = len - part.size
end
else
@prev_chunk = line
return false
end
2016-01-06 13:12:09 -05:00
end
2019-09-20 07:30:22 -04:00
if @in_last_chunk
set_ready
true
else
false
end
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
2016-01-06 13:12:09 -05:00
end
end
end