123456789_123456789_123456789_123456789_123456789_

Class: Puma::Client

Do not use. This class is for internal use only.
Relationships & Source Files
Super Chains via Extension / Inclusion / Inheritance
Instance Chain:
self, ClientEnv, Const
Inherits: Object
Defined in: lib/puma/client.rb

Overview

An instance of this class wraps a connection/socket. For example, this could be an http request from a browser or from CURL.

An instance of 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.

Instances of this class are responsible for knowing if the request line, headers and body are fully buffered and verified via the #try_to_finish method. All verification of each request is done in the Client object. They can be used to “time out” a response via the #timeout_at reader.

Most of the code for env processing and verification is contained in ClientEnv, which is included.

Constant Summary

Const - Included

BANNED_HEADER_KEY, CGI_VER, CHUNKED, CHUNK_SIZE, CLOSE, CLOSE_CHUNKED, CODE_NAME, COLON, CONNECTION_CLOSE, CONNECTION_KEEP_ALIVE, CONTENT_LENGTH, CONTENT_LENGTH2, CONTENT_LENGTH_S, CONTINUE, DQUOTE, EARLY_HINTS, ERROR_RESPONSE, GATEWAY_INTERFACE, HALT_COMMAND, HEAD, HIJACK, HIJACK_IO, HIJACK_P, HTTP, HTTPS, HTTPS_KEY, HTTP_10_200, HTTP_11, HTTP_11_100, HTTP_11_200, HTTP_CONNECTION, HTTP_EXPECT, HTTP_HEADER_DELIMITER, HTTP_HOST, HTTP_VERSION, HTTP_X_FORWARDED_FOR, HTTP_X_FORWARDED_PROTO, HTTP_X_FORWARDED_SCHEME, HTTP_X_FORWARDED_SSL, IANA_HTTP_METHODS, ILLEGAL_HEADER_KEY_REGEX, ILLEGAL_HEADER_VALUE_REGEX, KEEP_ALIVE, LINE_END, LOCALHOST, LOCALHOST_IPV4, LOCALHOST_IPV6, MAX_BODY, MAX_HEADER, NEWLINE, PATH_INFO, PORT_443, PORT_80, PROXY_PROTOCOL_V1_REGEX, PUMA_CONFIG, PUMA_PEERCERT, PUMA_SERVER_STRING, PUMA_SOCKET, PUMA_TMP_BASE, PUMA_VERSION, QUERY_STRING, RACK_AFTER_REPLY, RACK_INPUT, RACK_RESPONSE_FINISHED, RACK_URL_SCHEME, REMOTE_ADDR, REQUEST_METHOD, REQUEST_PATH, REQUEST_URI, RESTART_COMMAND, SERVER_NAME, SERVER_PORT, SERVER_PROTOCOL, SERVER_SOFTWARE, STOP_COMMAND, SUPPORTED_HTTP_METHODS, TRANSFER_ENCODING, TRANSFER_ENCODING2, TRANSFER_ENCODING_CHUNKED, UNMASKABLE_HEADERS, UNSPECIFIED_IPV4, UNSPECIFIED_IPV6, WRITE_TIMEOUT

ClientEnv - Included

HTTP_ON_VALUES

Class Method Summary

Instance Attribute Summary

Instance Method Summary

ClientEnv - Included

#default_server_port,
#normalize_env

Given a Hash #env for the request read from client, add and fixup keys to comply with Rack’s env guidelines.

#req_env_post_parse

Fixup any headers with , in the name to have _ now.

Constructor Details

.new(io, env = nil) ⇒ Client

[ GitHub ]

  
# File 'lib/puma/client.rb', line 83

def initialize(io, env=nil)
  @io = io
  @to_io = io.to_io
  @io_buffer = IOBuffer.new
  @proto_env = env
  @env = env&.dup

  @parser = HttpParser.new
  @parsed_bytes = 0
  @read_header = true
  @read_proxy = false
  @ready = false

  @body = nil
  @body_read_start = nil
  @buffer = nil
  @tempfile = nil

  @timeout_at = nil

  @requests_served = 0
  @hijacked = false

  @http_content_length_limit = nil
  @http_content_length_limit_exceeded = nil
  @error_status_code = nil

  @peerip = nil
  @peer_family = nil
  @listener = nil
  @remote_addr_header = nil
  @expect_proxy_proto = false

  @body_remain = 0

  @in_last_chunk = false

  # need unfrozen ASCII-8BIT, +'' is UTF-8
  @read_buffer = String.new # rubocop: disable Performance/UnfreezeString
end

Instance Attribute Details

#body (readonly)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 75

attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
            :tempfile, :io_buffer, :http_content_length_limit_exceeded,
            :requests_served, :error_status_code

#can_close?Boolean (readonly)

Returns true if the persistent connection can be closed immediately without waiting for the configured idle/shutdown timeout.

Version:

  • 5.0.0

[ GitHub ]

  
# File 'lib/puma/client.rb', line 372

def can_close?
  # Allow connection to close if we're not in the middle of parsing a request.
  @parsed_bytes == 0
end

#closed?Boolean (readonly)

Remove in ::Puma 7?

[ GitHub ]

  
# File 'lib/puma/client.rb', line 125

def closed?
  @to_io.closed?
end

#env (readonly)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 75

attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
            :tempfile, :io_buffer, :http_content_length_limit_exceeded,
            :requests_served, :error_status_code

#env_set_http_version (rw)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 81

attr_accessor :remote_addr_header, :listener, :env_set_http_version

#error_status_code (readonly)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 75

attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
            :tempfile, :io_buffer, :http_content_length_limit_exceeded,
            :requests_served, :error_status_code

#expect_proxy_proto=(val) (writeonly)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 377

def expect_proxy_proto=(val)
  if val
    if @read_header
      @read_proxy = true
    end
  else
    @read_proxy = false
  end
  @expect_proxy_proto = val
end

#has_back_to_back_requests?Boolean (readonly)

if a client sends back-to-back requests, the buffer may contain one or more of them.

[ GitHub ]

  
# File 'lib/puma/client.rb', line 190

def has_back_to_back_requests?
  !(@buffer.nil? || @buffer.empty?)
end

#hijacked (readonly)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 75

attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
            :tempfile, :io_buffer, :http_content_length_limit_exceeded,
            :requests_served, :error_status_code

#http_content_length_limit=(value) (writeonly)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 79

attr_writer :peerip, :http_content_length_limit, :supported_http_methods

#http_content_length_limit_exceeded (readonly)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 75

attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
            :tempfile, :io_buffer, :http_content_length_limit_exceeded,
            :requests_served, :error_status_code

#in_data_phase (readonly)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 148

def in_data_phase
  !(@read_header || @read_proxy)
end

#inspect (readonly)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 136

def inspect
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
end

#io (readonly)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 75

attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
            :tempfile, :io_buffer, :http_content_length_limit_exceeded,
            :requests_served, :error_status_code

#io_buffer (readonly)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 75

attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
            :tempfile, :io_buffer, :http_content_length_limit_exceeded,
            :requests_served, :error_status_code

#io_ok?Boolean (readonly)

Test to see if io meets a bare minimum of functioning, @to_io needs to be used for MiniSSL::Socket

[ GitHub ]

  
# File 'lib/puma/client.rb', line 131

def io_ok?
  @to_io.is_a?(::BasicSocket) && !closed?
end

#listener (rw)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 81

attr_accessor :remote_addr_header, :listener, :env_set_http_version

#peerip (rw)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 346

def peerip
  return @peerip if @peerip

  if @remote_addr_header
    hdr = (@env[@remote_addr_header] || @io.peeraddr.last).split(/[\s,]/).first
    @peerip = hdr
    return hdr
  end

  @peerip ||= @io.peeraddr.last
end

#peerip=(value) (rw)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 79

attr_writer :peerip, :http_content_length_limit, :supported_http_methods

#ready (readonly)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 75

attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
            :tempfile, :io_buffer, :http_content_length_limit_exceeded,
            :requests_served, :error_status_code

#remote_addr_header (rw)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 81

attr_accessor :remote_addr_header, :listener, :env_set_http_version

#requests_served (readonly)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 75

attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
            :tempfile, :io_buffer, :http_content_length_limit_exceeded,
            :requests_served, :error_status_code

#supported_http_methods=(value) (writeonly)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 79

attr_writer :peerip, :http_content_length_limit, :supported_http_methods

#tempfile (readonly)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 75

attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
            :tempfile, :io_buffer, :http_content_length_limit_exceeded,
            :requests_served, :error_status_code

#timeout (readonly)

Number of seconds until the timeout elapses.

[ GitHub ]

  
# File 'lib/puma/client.rb', line 158

def timeout
  [@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
end

#timeout_at (readonly)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 75

attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
            :tempfile, :io_buffer, :http_content_length_limit_exceeded,
            :requests_served, :error_status_code

#to_io (readonly)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 75

attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
            :tempfile, :io_buffer, :http_content_length_limit_exceeded,
            :requests_served, :error_status_code

Instance Method Details

#above_http_content_limit(value) (private)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 726

def above_http_content_limit(value)
  @http_content_length_limit_exceeded = (@http_content_length_limit&.< value)
end

#close

[ GitHub ]

  
# File 'lib/puma/client.rb', line 194

def close
  tempfile_close
  begin
    @io.close
  rescue IOError, Errno::EBADF
  end
end

#decode_chunk(chunk) (private)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 597

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
      end
      chunk = chunk[@partial_part_left..-1]
      @partial_part_left = 0
    else
      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
      @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?(CHUNK_VALID_ENDING)
      # Puma doesn't process chunk extensions, but should parse if they're
      # present, which is the reason for the semicolon regex
      chunk_hex = line.strip[/\A[^;]+/]
      if CHUNK_SIZE_INVALID.match? chunk_hex
        raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
      end
      len = chunk_hex.to_i(16)
      if len == 0
        @in_last_chunk = true
        @body.rewind
        rest = io.read
        if rest.bytesize < CHUNK_VALID_ENDING_SIZE
          @buffer = nil
          @partial_part_left = CHUNK_VALID_ENDING_SIZE - rest.bytesize
          return false
        else
          # if the next character is a CRLF, set buffer to everything after that CRLF
          start_of_rest = if rest.start_with?(CHUNK_VALID_ENDING)
            CHUNK_VALID_ENDING_SIZE
          else # we have started a trailer section, which we do not support. skip it!
            rest.index(CHUNK_VALID_ENDING*2) + CHUNK_VALID_ENDING_SIZE*2
          end

          @buffer = rest[start_of_rest..-1]
          @buffer = nil if @buffer.empty?
          set_ready
          return true
        end
      end

      # Track the excess as a function of the size of the
      # header vs the size of the actual data. Excess can
      # go negative (and is expected to) when the body is
      # significant.
      # The additional of chunk_hex.size and 2 compensates
      # for a client sending 1 byte in a chunked body over
      # a long period of time, making sure that that client
      # isn't accidentally eventually punished.
      @excess_cr += (line.size - len - chunk_hex.size - 2)

      if @excess_cr >= MAX_CHUNK_EXCESS
        raise HttpParserError, "Maximum chunk excess detected"
      end

      len += 2

      part = io.read(len)

      unless part
        @partial_part_left = len
        next
      end

      got = part.size

      case
      when got == len
        # proper chunked segment must end with "\r\n"
        if part.end_with? CHUNK_VALID_ENDING
          write_chunk(part[0..-3]) # to skip the ending \r\n
        else
          raise HttpParserError, "Chunk size mismatch"
        end
      when got <= len - 2
        write_chunk(part)
        @partial_part_left = len - part.size
      when got == len - 1 # edge where we get just \r but not \n
        write_chunk(part[0..-2])
        @partial_part_left = len - part.size
      end
    else
      if @prev_chunk.size + line.size >= MAX_CHUNK_HEADER_SIZE
        raise HttpParserError, "maximum size of chunk header exceeded"
      end

      @prev_chunk = line
      return false
    end
  end

  if @in_last_chunk
    set_ready
    true
  else
    false
  end
end

#eagerly_finish

[ GitHub ]

  
# File 'lib/puma/client.rb', line 269

def eagerly_finish
  return true if @ready
  while @to_io.wait_readable(0) # rubocop: disable Style/WhileUntilModifier
    return true if try_to_finish
  end
  false
end

#finish(timeout)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 277

def finish(timeout)
  return if @ready
  @to_io.wait_readable(timeout) || timeout! until try_to_finish
end

#full_hijack

For the full hijack protocol, 'rack.hijack' is set to client.method :full_hijack

[ GitHub ]

  
# File 'lib/puma/client.rb', line 142

def full_hijack
  @hijacked = true
  env[HIJACK_IO] ||= @io
end

#parser_executeInteger

Wraps @parser.execute and adds meaningful error messages

Returns:

  • (Integer)

    bytes of buffer read by parser

[ GitHub ]

  
# File 'lib/puma/client.rb', line 285

def parser_execute
  ret = @parser.execute(@env, @buffer, @parsed_bytes)

  if @env[REQUEST_METHOD] && @supported_http_methods != :any && !@supported_http_methods.key?(@env[REQUEST_METHOD])
    raise HttpParserError501, "#{@env[REQUEST_METHOD]} method is not supported"
  end
  ret
rescue => e
  @env[HTTP_CONNECTION] = 'close'
  raise e unless HttpParserError === e && e.message.include?('non-SSL')

  req, _ = @buffer.split "\r\n\r\n"
  request_line, headers = req.split "\r\n", 2

  # below checks for request issues and changes error message accordingly
  if !@env.key? REQUEST_METHOD
    if request_line.count(' ') != 2
       # maybe this is an SSL connection ?
      raise e
    else
      method = request_line[/\A[^ ]+/]
      raise e, "Invalid HTTP format, parsing fails. Bad method #{method}"
    end
  elsif !@env.key? REQUEST_PATH
    path = request_line[/\A[^ ]+ +([^ ?\r\n]+)/, 1]
    raise e, "Invalid HTTP format, parsing fails. Bad path #{path}"
  elsif request_line.match?(/\A[^ ]+ +[^ ?\r\n]+\?/) && !@env.key?(QUERY_STRING)
    query = request_line[/\A[^ ]+ +[^? ]+\?([^ ]+)/, 1]
    raise e, "Invalid HTTP format, parsing fails. Bad query #{query}"
  elsif !@env.key? SERVER_PROTOCOL
    # protocol is bad
    text = request_line[/[^ ]*\z/]
    raise HttpParserError, "Invalid HTTP format, parsing fails. Bad protocol #{text}"
  elsif !headers.empty?
    # headers are bad
    hdrs = headers.split("\r\n").map { |h| h.gsub "\n", '\n'}.join "\n"
    raise HttpParserError, "Invalid HTTP format, parsing fails. Bad headers\n#{hdrs}"
  end
end

#peer_family

[ GitHub ]

  
# File 'lib/puma/client.rb', line 358

def peer_family
  return @peer_family if @peer_family

  @peer_family ||= begin
                     @io.local_address.afamily
                   rescue
                     Socket::AF_INET
                   end
end

#process_back_to_back_requests

only used with back-to-back requests contained in the buffer

[ GitHub ]

  
# File 'lib/puma/client.rb', line 178

def process_back_to_back_requests
  if @buffer
    return false unless try_to_parse_proxy_protocol

    @parsed_bytes = parser_execute

    @parser.finished? ? process_env_body : false
  end
end

#process_env_body

processes the #env and the request body

Raises:

[ GitHub ]

  
# File 'lib/puma/client.rb', line 326

def process_env_body
  temp = setup_body
  normalize_env
  req_env_post_parse
  raise HttpParserError if @error_status_code
  temp
end

#read_body (private)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 501

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

  # don't bother with reading zero bytes
  unless remain.zero?
    begin
      chunk = @io.read_nonblock(remain.clamp(0, CHUNK_SIZE), @read_buffer)
    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

    remain -= @body.write(chunk)
  end

  if remain <= 0
    @body.rewind
    @buffer = nil
    set_ready
    true
  else
    @body_remain = remain
    false
  end
end

#read_chunked_body (private)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 542

def read_chunked_body
  while true
    begin
      chunk = @io.read_nonblock(CHUNK_SIZE, @read_buffer)
    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.to_s
      return true
    end
  end
end

#reset

[ GitHub ]

  
# File 'lib/puma/client.rb', line 162

def reset
  @parser.reset
  @io_buffer.reset
  @read_header = true
  @read_proxy = !!@expect_proxy_proto
  @env = @proto_env.dup
  @parsed_bytes = 0
  @ready = false
  @body_remain = 0
  @peerip = nil if @remote_addr_header
  @in_last_chunk = false
  @http_content_length_limit_exceeded = nil
  @error_status_code = nil
end

#set_ready (private)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 718

def set_ready
  if @body_read_start
    @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - @body_read_start
  end
  @requests_served += 1
  @ready = true
end

#set_timeout(val)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 152

def set_timeout(val)
  @timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + val
end

#setup_bodyBoolean (private)

Checks the request Transfer-Encoding and/or Content-Length to see if they are valid. Raises errors if not, otherwise reads the body.

Returns:

  • (Boolean)

    true if the body can be completely read, false otherwise

[ GitHub ]

  
# File 'lib/puma/client.rb', line 394

def setup_body
  @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_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

  parser_body = @parser.body

  te = @env[TRANSFER_ENCODING2]
  if te
    te_lwr = te.downcase
    if te.include? ','
      te_ary = te_lwr.split(',').each { |te| te.gsub!(STRIP_OWS, "") }
      te_count = te_ary.count CHUNKED
      te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
      if te_count > 1
        raise HttpParserError   , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
      elsif te_ary.last != CHUNKED
        raise HttpParserError   , "#{TE_ERR_MSG}, last value must be chunked: '#{te}'"
      elsif !te_valid
        raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
      end
      @env.delete TRANSFER_ENCODING2
      return setup_chunked_body parser_body
    elsif te_lwr == CHUNKED
      @env.delete TRANSFER_ENCODING2
      return setup_chunked_body parser_body
    elsif ALLOWED_TRANSFER_ENCODING.include? te_lwr
      raise HttpParserError     , "#{TE_ERR_MSG}, single value must be chunked: '#{te}'"
    else
      raise HttpParserError501  , "#{TE_ERR_MSG}, unknown value: '#{te}'"
    end
  end

  @chunked_body = false

  cl = @env[CONTENT_LENGTH]

  if cl
    # cannot contain characters that are not \d, or be empty
    if CONTENT_LENGTH_VALUE_INVALID.match?(cl) || cl.empty?
      @error_status_code = 400
      @env[HTTP_CONNECTION] = 'close'
      raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
    end
  else
    @buffer = parser_body.empty? ? nil : parser_body
    @body = EmptyBody
    set_ready
    return true
  end

  content_length = cl.to_i

  if above_http_content_limit(content_length)
    @buffer = nil
    @body = EmptyBody
    @error_status_code = 413
    @env[HTTP_CONNECTION] = 'close'
    raise HttpParserError, "Payload Too Large"
  end

  remain = content_length - parser_body.bytesize

  if remain <= 0
    # Part of the parser_body is a pipelined request OR garbage. We'll deal with that later.
    if content_length == 0
      @body = EmptyBody
      if parser_body.empty?
        @buffer = nil
      else
        @buffer = parser_body
      end
    elsif remain == 0
      @body = StringIO.new parser_body
      @buffer = nil
    else
      @body = StringIO.new(parser_body[0,content_length])
      @buffer = parser_body[content_length..-1]
    end
    set_ready
    return true
  end

  if remain > MAX_BODY
    @body = Tempfile.create(Const::PUMA_TMP_BASE)
    File.unlink @body.path unless IS_WINDOWS
    @body.binmode
    @tempfile = @body
  else
    # The parser_body[0,0] trick is to get an empty string in the same
    # encoding as parser_body.
    @body = StringIO.new parser_body[0,0]
  end

  @body.write parser_body

  @body_remain = remain

  false
end

#setup_chunked_body(body) (private)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 567

def setup_chunked_body(body)
  @chunked_body = true
  @partial_part_left = 0
  @prev_chunk = ""
  @excess_cr = 0

  @body = Tempfile.create(Const::PUMA_TMP_BASE)
  File.unlink @body.path unless IS_WINDOWS
  @body.binmode
  @tempfile = @body
  @chunked_content_length = 0

  if decode_chunk(body)
    @env[CONTENT_LENGTH] = @chunked_content_length.to_s
    return true
  end
end

#tempfile_close

[ GitHub ]

  
# File 'lib/puma/client.rb', line 202

def tempfile_close
  tf_path = @tempfile&.path
  @tempfile&.close
  File.unlink(tf_path) if tf_path
  @tempfile = nil
  @body = nil
rescue Errno::ENOENT, IOError
end

#timeout!

Raises:

[ GitHub ]

  
# File 'lib/puma/client.rb', line 334

def timeout!
  write_error(408) if in_data_phase
  raise ConnectionError
end

#try_to_finish

[ GitHub ]

  
# File 'lib/puma/client.rb', line 235

def try_to_finish
  return read_body if in_data_phase

  data = nil
  begin
    data = @io.read_nonblock(CHUNK_SIZE)
  rescue IO::WaitReadable
    return false
  rescue EOFError
    # Swallow error, don't log
  rescue SystemCallError, IOError
    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

  return false unless try_to_parse_proxy_protocol

  @parsed_bytes = parser_execute

  @parser.finished? ? process_env_body : false
end

#try_to_parse_proxy_protocol

If necessary, read the PROXY protocol from the buffer. Returns false if more data is needed.

[ GitHub ]

  
# File 'lib/puma/client.rb', line 213

def try_to_parse_proxy_protocol
  if @read_proxy
    if @expect_proxy_proto == :v1
      if @buffer.include? "\r\n"
        if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
          if md[1]
            @peerip = md[1].split(" ")[0]
          end
          @buffer = md.post_match
        end
        # if the buffer has a \r\n but doesn't have a PROXY protocol
        # request, this is just HTTP from a non-PROXY client; move on
        @read_proxy = false
        return @buffer.size > 0
      else
        return false
      end
    end
  end
  true
end

#write_chunk(str) (private)

Version:

  • 5.0.0

[ GitHub ]

  
# File 'lib/puma/client.rb', line 586

def write_chunk(str)
  if above_http_content_limit(@chunked_content_length + str.bytesize)
    @buffer = nil
    @body = EmptyBody
    @error_status_code = 413
    @env[HTTP_CONNECTION] = 'close'
    raise HttpParserError, "Payload Too Large"
  end
  @chunked_content_length += @body.write(str)
end

#write_error(status_code)

[ GitHub ]

  
# File 'lib/puma/client.rb', line 339

def write_error(status_code)
  begin
    @io << ERROR_RESPONSE[status_code]
  rescue StandardError
  end
end