Class: Puma::Client

Inherits:
Object
  • Object
show all
Includes:
Const
Defined in:
lib/puma/client.rb

Overview

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.

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.

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 collapse

ALLOWED_TRANSFER_ENCODING =

this tests all values but the last, which must be chunked

%w[compress deflate gzip].freeze
CHUNK_SIZE_INVALID =

chunked body validation

/[^\h]/.freeze
CHUNK_VALID_ENDING =
Const::LINE_END
CHUNK_VALID_ENDING_SIZE =
CHUNK_VALID_ENDING.bytesize
MAX_CHUNK_HEADER_SIZE =

The maximum number of bytes we’ll buffer looking for a valid chunk header.

4096
MAX_CHUNK_EXCESS =

The maximum amount of excess data the client sends using chunk size extensions before we abort the connection.

16 * 1024
CONTENT_LENGTH_VALUE_INVALID =

Content-Length header value validation

/[^\d]/.freeze
TE_ERR_MSG =
'Invalid Transfer-Encoding'
EmptyBody =

The object used for a request with no body. All requests with no body share this one object since it has no state.

NullIO.new

Constants included from Const

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

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(io, env = nil) ⇒ Client

Returns a new instance of Client.



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/puma/client.rb', line 73

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 = false

  @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

#bodyObject (readonly)

Returns the value of attribute body.



113
114
115
# File 'lib/puma/client.rb', line 113

def body
  @body
end

#envObject (readonly)

Returns the value of attribute env.



113
114
115
# File 'lib/puma/client.rb', line 113

def env
  @env
end

#hijackedObject (readonly)

Returns the value of attribute hijacked.



113
114
115
# File 'lib/puma/client.rb', line 113

def hijacked
  @hijacked
end

#http_content_length_limit=(value) ⇒ Object (writeonly)

Sets the attribute http_content_length_limit

Parameters:

  • value

    the value to set the attribute http_content_length_limit to.



116
117
118
# File 'lib/puma/client.rb', line 116

def http_content_length_limit=(value)
  @http_content_length_limit = value
end

#http_content_length_limit_exceededObject (readonly)

Returns the value of attribute http_content_length_limit_exceeded.



113
114
115
# File 'lib/puma/client.rb', line 113

def http_content_length_limit_exceeded
  @http_content_length_limit_exceeded
end

#in_data_phaseObject (readonly)



144
145
146
# File 'lib/puma/client.rb', line 144

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

#inspectObject (readonly)



132
133
134
# File 'lib/puma/client.rb', line 132

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

#ioObject (readonly)

Returns the value of attribute io.



113
114
115
# File 'lib/puma/client.rb', line 113

def io
  @io
end

#io_bufferObject (readonly)

Returns the value of attribute io_buffer.



113
114
115
# File 'lib/puma/client.rb', line 113

def io_buffer
  @io_buffer
end

#listenerObject

Returns the value of attribute listener.



118
119
120
# File 'lib/puma/client.rb', line 118

def listener
  @listener
end

#peeripObject



315
316
317
318
319
320
321
322
323
324
325
# File 'lib/puma/client.rb', line 315

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

#readyObject (readonly)

Returns the value of attribute ready.



113
114
115
# File 'lib/puma/client.rb', line 113

def ready
  @ready
end

#remote_addr_headerObject

Returns the value of attribute remote_addr_header.



118
119
120
# File 'lib/puma/client.rb', line 118

def remote_addr_header
  @remote_addr_header
end

#tempfileObject (readonly)

Returns the value of attribute tempfile.



113
114
115
# File 'lib/puma/client.rb', line 113

def tempfile
  @tempfile
end

#timeout_atObject (readonly)

Returns the value of attribute timeout_at.



113
114
115
# File 'lib/puma/client.rb', line 113

def timeout_at
  @timeout_at
end

#to_ioObject (readonly)

Returns the value of attribute to_io.



113
114
115
# File 'lib/puma/client.rb', line 113

def to_io
  @to_io
end

Instance Method Details

#callObject

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



138
139
140
141
# File 'lib/puma/client.rb', line 138

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

#can_close?Boolean

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

Returns:

  • (Boolean)

Version:

  • 5.0.0



341
342
343
344
# File 'lib/puma/client.rb', line 341

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

#closeObject



194
195
196
197
198
199
200
201
# File 'lib/puma/client.rb', line 194

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

#closed?Boolean

Remove in Puma 7?

Returns:

  • (Boolean)


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

def closed?
  @to_io.closed?
end

#eagerly_finishObject



292
293
294
295
296
# File 'lib/puma/client.rb', line 292

def eagerly_finish
  return true if @ready
  return false unless @to_io.wait_readable(0)
  try_to_finish
end

#expect_proxy_proto=(val) ⇒ Object



346
347
348
349
350
351
352
353
354
355
# File 'lib/puma/client.rb', line 346

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

#finish(timeout) ⇒ Object



298
299
300
301
# File 'lib/puma/client.rb', line 298

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

#io_ok?Boolean

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

Returns:

  • (Boolean)


127
128
129
# File 'lib/puma/client.rb', line 127

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

#peer_familyObject



327
328
329
330
331
332
333
334
335
# File 'lib/puma/client.rb', line 327

def peer_family
  return @peer_family if @peer_family

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

#reset(fast_check = true) ⇒ Object



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/puma/client.rb', line 157

def reset(fast_check=true)
  @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 = false

  if @buffer
    return false unless try_to_parse_proxy_protocol

    @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 && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
        return try_to_finish
      end
    rescue IOError
      # swallow it
    end
  end
end

#set_timeout(val) ⇒ Object



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

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

#tempfile_closeObject



203
204
205
206
207
208
209
210
# File 'lib/puma/client.rb', line 203

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

#timeoutObject

Number of seconds until the timeout elapses.



153
154
155
# File 'lib/puma/client.rb', line 153

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

#timeout!Object

Raises:



303
304
305
306
# File 'lib/puma/client.rb', line 303

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

#try_to_finishObject



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/puma/client.rb', line 236

def try_to_finish
  if env[CONTENT_LENGTH] && above_http_content_limit(env[CONTENT_LENGTH].to_i)
    @http_content_length_limit_exceeded = true
  end

  if @http_content_length_limit_exceeded
    @buffer = nil
    @body = EmptyBody
    set_ready
    return true
  end

  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(@env, @buffer, @parsed_bytes)

  if @parser.finished? && above_http_content_limit(@parser.body.bytesize)
    @http_content_length_limit_exceeded = true
  end

  if @parser.finished?
    return setup_body
  elsif @parsed_bytes >= MAX_HEADER
    raise HttpParserError,
      "HEADER is longer than allowed, aborting client early."
  end

  false
end

#try_to_parse_proxy_protocolObject

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



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/puma/client.rb', line 214

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_error(status_code) ⇒ Object



308
309
310
311
312
313
# File 'lib/puma/client.rb', line 308

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