123456789_123456789_123456789_123456789_123456789_

Class: Puma::Client

Relationships & Source Files
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
self, Delegation
Instance Chain:
self, Const
Inherits: Object
Defined in: lib/puma/client.rb

Overview

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 for example it is passed into `IO.select` inside of the `Puma::Reactor`. This is accomplished by the `to_io` method which gets called on any non-IO objects being used with the ::IO api such as `IO.select.

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.

Constant Summary

Const - Included

CGI_VER, CHUNKED, CHUNK_SIZE, CLOSE, CLOSE_CHUNKED, CODE_NAME, COLON, CONNECTION_CLOSE, CONNECTION_KEEP_ALIVE, CONTENT_LENGTH, CONTENT_LENGTH2, CONTENT_LENGTH_S, CONTINUE, EARLY_HINTS, ERROR_400_RESPONSE, ERROR_404_RESPONSE, ERROR_408_RESPONSE, ERROR_500_RESPONSE, ERROR_503_RESPONSE, FAST_TRACK_KA_TIMEOUT, FIRST_DATA_TIMEOUT, 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_HOST, HTTP_VERSION, HTTP_X_FORWARDED_FOR, KEEP_ALIVE, LINE_END, LOCALHOST, LOCALHOST_ADDR, LOCALHOST_IP, MAX_BODY, MAX_HEADER, NEWLINE, PATH_INFO, PERSISTENT_TIMEOUT, PORT_443, PORT_80, PUMA_CONFIG, PUMA_PEERCERT, PUMA_SERVER_STRING, PUMA_SOCKET, PUMA_TMP_BASE, PUMA_VERSION, QUERY_STRING, RACK_AFTER_REPLY, RACK_INPUT, RACK_URL_SCHEME, REMOTE_ADDR, REQUEST_METHOD, REQUEST_PATH, REQUEST_URI, RESTART_COMMAND, SERVER_NAME, SERVER_PORT, SERVER_PROTOCOL, SERVER_SOFTWARE, STOP_COMMAND, TRANSFER_ENCODING, TRANSFER_ENCODING2, TRANSFER_ENCODING_CHUNKED, WRITE_TIMEOUT

Class Method Summary

Instance Attribute Summary

Instance Method Summary

Constructor Details

.new(io, env = nil) ⇒ Client

[ GitHub ]

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

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
  @buffer = nil
  @tempfile = nil

  @timeout_at = nil

  @requests_served = 0
  @hijacked = false

  @peerip = nil
  @remote_addr_header = nil
end

Instance Attribute Details

#body (readonly)

[ GitHub ]

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

attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
            :tempfile

#env (readonly)

[ GitHub ]

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

attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
            :tempfile

#hijacked (readonly)

[ GitHub ]

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

attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
            :tempfile

#io (readonly)

[ GitHub ]

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

attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
            :tempfile

#peerip (rw)

[ GitHub ]

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

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

#peerip=(value) (rw)

[ GitHub ]

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

attr_writer :peerip

#ready (readonly)

[ GitHub ]

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

attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
            :tempfile

#remote_addr_header (rw)

[ GitHub ]

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

attr_accessor :remote_addr_header

#tempfile (readonly)

[ GitHub ]

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

attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
            :tempfile

#timeout_at (readonly)

[ GitHub ]

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

attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
            :tempfile

#to_io (readonly)

[ GitHub ]

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

attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
            :tempfile

Instance Method Details

#call

For the hijack protocol (allows us to just put the Client object into the env)

[ GitHub ]

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

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

#close

[ GitHub ]

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

def close
  begin
    @io.close
  rescue IOError
    Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
  end
end

#decode_chunk(chunk)

[ GitHub ]

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

def decode_chunk(chunk)
  if @partial_part_left > 0
    if @partial_part_left <= chunk.size
      @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
      chunk = chunk[@partial_part_left..-1]
    else
      @body << chunk
      @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
        @body.rewind
        rest = io.read
        rest = rest[2..-1] if rest.start_with?("\r\n")
        @buffer = rest.empty? ? nil : rest
        @requests_served += 1
        @ready = true
        return true
      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

  return false
end

#eagerly_finish

See additional method definition at line 364.

[ GitHub ]

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

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

#finish

[ GitHub ]

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

def finish
  return true if @ready
  until try_to_finish
    IO.select([@to_io], nil, nil)
  end
  true
end

#in_data_phase

[ GitHub ]

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

def in_data_phase
  !@read_header
end

#inspect

[ GitHub ]

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

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

#jruby_start_try_to_finish

[ GitHub ]

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

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
    @requests_served += 1
    @ready = true
    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

#read_body

[ GitHub ]

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

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
    @requests_served += 1
    @ready = true
    raise EOFError
  end

  remain -= @body.write(chunk)

  if remain <= 0
    @body.rewind
    @buffer = nil
    @requests_served += 1
    @ready = true
    return true
  end

  @body_remain = remain

  false
end

#read_chunked_body

[ GitHub ]

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

def read_chunked_body
  while true
    begin
      chunk = @io.read_nonblock(4096)
    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
      @requests_served += 1
      @ready = true
      raise EOFError
    end

    return true if decode_chunk(chunk)
  end
end

#reset(fast_check = true)

[ GitHub ]

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

def reset(fast_check=true)
  @parser.reset
  @read_header = true
  @env = @proto_env.dup
  @body = nil
  @tempfile = nil
  @parsed_bytes = 0
  @ready = 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
  elsif fast_check &&
        IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
    return try_to_finish
  end
end

#set_timeout(val)

[ GitHub ]

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

def set_timeout(val)
  @timeout_at = Time.now + val
end

#setup_body

[ GitHub ]

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

def setup_body
  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
    @requests_served += 1
    @ready = true
    return true
  end

  remain = cl.to_i - body.bytesize

  if remain <= 0
    @body = StringIO.new(body)
    @buffer = nil
    @requests_served += 1
    @ready = true
    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

#setup_chunked_body(body)

[ GitHub ]

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

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

#try_to_finish

[ GitHub ]

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

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
    raise ConnectionError, "Connection error detected during read"
  end

  # No data means a closed socket
  unless data
    @buffer = nil
    @requests_served += 1
    @ready = true
    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

#write_400

[ GitHub ]

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

def write_400
  begin
    @io << ERROR_400_RESPONSE
  rescue StandardError
  end
end

#write_408

[ GitHub ]

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

def write_408
  begin
    @io << ERROR_408_RESPONSE
  rescue StandardError
  end
end

#write_500

[ GitHub ]

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

def write_500
  begin
    @io << ERROR_500_RESPONSE
  rescue StandardError
  end
end