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'
STRIP_OWS =
/\A[ \t]+|[ \t]+\z/
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::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_RESPONSE_FINISHED, 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.



70
71
72
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
# File 'lib/puma/client.rb', line 70

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.



110
111
112
# File 'lib/puma/client.rb', line 110

def body
  @body
end

#envObject (readonly)

Returns the value of attribute env.



110
111
112
# File 'lib/puma/client.rb', line 110

def env
  @env
end

#hijackedObject (readonly)

Returns the value of attribute hijacked.



110
111
112
# File 'lib/puma/client.rb', line 110

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.



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

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.



110
111
112
# File 'lib/puma/client.rb', line 110

def http_content_length_limit_exceeded
  @http_content_length_limit_exceeded
end

#in_data_phaseObject (readonly)



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

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

#inspectObject (readonly)



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

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

#ioObject (readonly)

Returns the value of attribute io.



110
111
112
# File 'lib/puma/client.rb', line 110

def io
  @io
end

#io_bufferObject (readonly)

Returns the value of attribute io_buffer.



110
111
112
# File 'lib/puma/client.rb', line 110

def io_buffer
  @io_buffer
end

#listenerObject

Returns the value of attribute listener.



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

def listener
  @listener
end

#peeripObject



352
353
354
355
356
357
358
359
360
361
362
# File 'lib/puma/client.rb', line 352

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.



110
111
112
# File 'lib/puma/client.rb', line 110

def ready
  @ready
end

#remote_addr_headerObject

Returns the value of attribute remote_addr_header.



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

def remote_addr_header
  @remote_addr_header
end

#requests_servedObject (readonly)

Returns the value of attribute requests_served.



110
111
112
# File 'lib/puma/client.rb', line 110

def requests_served
  @requests_served
end

#tempfileObject (readonly)

Returns the value of attribute tempfile.



110
111
112
# File 'lib/puma/client.rb', line 110

def tempfile
  @tempfile
end

#timeoutObject (readonly)

Number of seconds until the timeout elapses.



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

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

#timeout_atObject (readonly)

Returns the value of attribute timeout_at.



110
111
112
# File 'lib/puma/client.rb', line 110

def timeout_at
  @timeout_at
end

#to_ioObject (readonly)

Returns the value of attribute to_io.



110
111
112
# File 'lib/puma/client.rb', line 110

def to_io
  @to_io
end

Instance Method Details

#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



378
379
380
381
# File 'lib/puma/client.rb', line 378

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

#closeObject



192
193
194
195
196
197
198
# File 'lib/puma/client.rb', line 192

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

#closed?Boolean

Remove in Puma 7?

Returns:

  • (Boolean)


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

def closed?
  @to_io.closed?
end

#eagerly_finishObject



289
290
291
292
293
294
295
# File 'lib/puma/client.rb', line 289

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

#expect_proxy_proto=(val) ⇒ Object



383
384
385
386
387
388
389
390
391
392
# File 'lib/puma/client.rb', line 383

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



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

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

#full_hijackObject

For the full hijack protocol, ‘env` is set to `client.method :full_hijack`



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

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

#has_back_to_back_requests?Boolean

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

Returns:

  • (Boolean)


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

def has_back_to_back_requests?
  !(@buffer.nil? || @buffer.empty?)
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)


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

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

#parser_executeInteger

Wraps ‘@parser.execute` and adds meaningful error messages

Returns:

  • (Integer)

    bytes of buffer read by parser



305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/puma/client.rb', line 305

def parser_execute
  @parser.execute(@env, @buffer, @parsed_bytes)
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_familyObject



364
365
366
367
368
369
370
371
372
# File 'lib/puma/client.rb', line 364

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_requestsObject

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



171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/puma/client.rb', line 171

def process_back_to_back_requests
  if @buffer
    return false unless try_to_parse_proxy_protocol

    @parsed_bytes = parser_execute

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

#resetObject



156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/puma/client.rb', line 156

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

#set_timeout(val) ⇒ Object



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

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

#tempfile_closeObject



200
201
202
203
204
205
206
207
# File 'lib/puma/client.rb', line 200

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!Object

Raises:



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

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

#try_to_finishObject



233
234
235
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
# File 'lib/puma/client.rb', line 233

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

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

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

#try_to_parse_proxy_protocolObject

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



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

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



345
346
347
348
349
350
# File 'lib/puma/client.rb', line 345

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